* 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>
6.2 KiB
AGENTS.md
Project Overview
gws is a Rust CLI tool for interacting with Google Workspace APIs. It dynamically generates its command surface at runtime by parsing Google Discovery Service JSON documents.
Important
Dynamic Discovery: This project does NOT use generated Rust crates (e.g.,
google-drive3) for API interaction. Instead, it fetches the Discovery JSON at runtime and buildsclapcommands dynamically. When adding a new service, you only need to register it insrc/services.rsand verify the Discovery URL pattern insrc/discovery.rs. Do NOT add new crates toCargo.tomlfor standard Google APIs.
Note
Package Manager: Use
pnpminstead ofnpmfor Node.js package management in this repository.
Build & Test
cargo build # Build in dev mode
cargo clippy -- -D warnings # Lint check
cargo test # Run tests
Changesets
Every PR must include a changeset file. Create one at .changeset/<descriptive-name>.md:
---
"@googleworkspace/cli": patch
---
Brief description of the change
Use patch for fixes/chores, minor for new features, major for breaking changes. The CI policy check will fail without a changeset.
Architecture
The CLI uses a two-phase argument parsing strategy:
- Parse argv to extract the service name (e.g.,
drive) - Fetch the service's Discovery Document, build a dynamic
clap::Commandtree, then re-parse
Source Layout
| File | Purpose |
|---|---|
src/main.rs |
Entrypoint, two-phase CLI parsing, method resolution |
src/discovery.rs |
Serde models for Discovery Document + fetch/cache |
src/services.rs |
Service alias → Discovery API name/version mapping |
src/auth.rs |
Headless OAuth2 via yup-oauth2 |
src/commands.rs |
Recursive clap::Command builder from Discovery resources |
src/executor.rs |
HTTP request construction, response handling, schema validation |
src/schema.rs |
gws schema command — introspect API method schemas |
src/error.rs |
Structured JSON error output |
Demo Videos
Demo recordings are generated with VHS (.tape files).
vhs docs/demo.tape
VHS quoting rules
- Use double quotes for simple strings:
Type "gws --help" Enter - Use backtick quotes when the typed text contains JSON with double quotes:
Type `gws drive files list --params '{"pageSize":5}'` Enter\"escapes inside double-quotedTypestrings are not supported by VHS and will cause parse errors.
Scene art
ASCII art title cards live in art/. The scripts/show-art.sh helper clears the screen and cats the file. Portrait scenes use scene*.txt; landscape chapters use long-*.txt.
Input Validation & URL Safety
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.
Path Safety (src/validate.rs)
When adding new helpers or CLI flags that accept file paths, always validate using the shared helpers:
| Scenario | Validator | Rejects |
|---|---|---|
File path for writing (--output-dir) |
validate::validate_safe_output_dir() |
Absolute paths, ../ traversal, symlinks outside CWD, control chars |
File path for reading (--dir) |
validate::validate_safe_dir_path() |
Absolute paths, ../ traversal, symlinks outside CWD, control chars |
Enum/allowlist values (--msg-format) |
clap value_parser (see gmail/mod.rs) |
Any value not in the allowlist |
// In your argument parser:
if let Some(output_dir) = matches.get_one::<String>("output-dir") {
crate::validate::validate_safe_output_dir(output_dir)?;
builder.output_dir(Some(output_dir.clone()));
}
URL Encoding (src/helpers/mod.rs)
User-supplied values embedded in URL path segments must be percent-encoded. Use the shared helper:
// CORRECT — encodes slashes, spaces, and special characters
let url = format!(
"https://www.googleapis.com/drive/v3/files/{}",
crate::helpers::encode_path_segment(file_id),
);
// WRONG — raw user input in URL path
let url = format!("https://www.googleapis.com/drive/v3/files/{}", file_id);
For query parameters, use reqwest's .query() builder which handles encoding automatically:
// CORRECT — reqwest encodes query values
client.get(url).query(&[("q", user_query)]).send().await?;
// WRONG — manual string interpolation in query strings
let url = format!("{}?q={}", base_url, user_query);
Resource Name Validation (src/helpers/mod.rs)
When a user-supplied string is used as a GCP resource identifier (project ID, topic name, space name, etc.) that gets embedded in a URL path, validate it first:
// Validates the string does not contain path traversal segments (`..`), control characters, or URL-breaking characters like `?` and `#`.
let project = crate::helpers::validate_resource_name(&project_id)?;
let url = format!("https://pubsub.googleapis.com/v1/projects/{}/topics/my-topic", project);
This prevents injection of query parameters, path traversal, or other malicious payloads through resource name arguments like --project or --space.
Checklist for New Features
When adding a new helper or CLI command:
- File paths → Use
validate_safe_output_dir/validate_safe_dir_path - Enum flags → Constrain via clap
value_parserorvalidate_msg_format - URL path segments → Use
encode_path_segment() - Query parameters → Use reqwest
.query()builder - Resource names (project IDs, space names, topic names) → Use
validate_resource_name() - Write tests for both the happy path AND the rejection path (e.g., pass
../../.sshand assertErr)
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/)- Supports
.envfiles viadotenvy