Files
googleworkspace__cli/AGENTS.md
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

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 builds clap commands dynamically. When adding a new service, you only need to register it in src/services.rs and verify the Discovery URL pattern in src/discovery.rs. Do NOT add new crates to Cargo.toml for standard Google APIs.

Note

Package Manager: Use pnpm instead of npm for 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:

  1. Parse argv to extract the service name (e.g., drive)
  2. Fetch the service's Discovery Document, build a dynamic clap::Command tree, 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-quoted Type strings 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:

  1. File paths → Use validate_safe_output_dir / validate_safe_dir_path
  2. Enum flags → Constrain via clap value_parser or validate_msg_format
  3. URL path segments → Use encode_path_segment()
  4. Query parameters → Use reqwest .query() builder
  5. Resource names (project IDs, space names, topic names) → Use validate_resource_name()
  6. Write tests for both the happy path AND the rejection path (e.g., pass ../../.ssh and assert Err)

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 .env files via dotenvy