Skip to content

feat: Google Workspace tools via gogcli-rs (Gmail + Calendar)#162

Merged
qhkm merged 8 commits intomainfrom
feat/google-workspace-tools
Feb 26, 2026
Merged

feat: Google Workspace tools via gogcli-rs (Gmail + Calendar)#162
qhkm merged 8 commits intomainfrom
feat/google-workspace-tools

Conversation

@qhkm
Copy link
Copy Markdown
Owner

@qhkm qhkm commented Feb 26, 2026

Summary

  • Add Gmail + Calendar tools to ZeptoClaw via qhkm/gogcli-rs crates
  • Feature-gated behind --features google — default binary unchanged
  • Single google tool with 7 actions: gmail_search, gmail_read, gmail_send, gmail_reply, calendar_list, calendar_create, calendar_freebusy
  • Auth auto-detect: stored OAuth token → fallback to config access_token
  • Write actions (gmail_send, gmail_reply, calendar_create) require ApprovalGate confirmation

Closes #160

Changes

File Change
Cargo.toml Add google feature + gogcli-rs git deps
src/tools/google.rs New — GoogleTool with 7 actions (22 unit tests)
src/tools/mod.rs Feature-gated module + re-export
src/config/types.rs GoogleToolConfig struct + ToolsConfig field
src/config/mod.rs Env overrides for ZEPTOCLAW_TOOLS_GOOGLE_*
src/cli/common.rs Tool registration + resolve_google_token() helper
src/auth/mod.rs Google OAuth endpoints + scopes
src/cli/tools.rs Tool status display entry
src/tools/approval.rs Added to default dangerous tools
src/lib.rs Feature-gated re-export
CLAUDE.md Architecture + feature docs

Test plan

  • cargo fmt -- --check clean
  • cargo clippy --features google -- -D warnings — 0 warnings
  • cargo test --lib --features google — 2603 passed, 0 failed
  • Default build (cargo check) unaffected — no binary size change
  • Manual: zeptoclaw auth login google → OAuth flow
  • Manual: zeptoclaw agent --features google -m "search my inbox" → tool invoked

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Google Workspace integration with Gmail and Google Calendar support
    • New tool enables: search and read emails, send and reply to messages, list and create calendar events, check availability slots
    • Google OAuth authentication support via access tokens and OAuth flow
    • Configurable default calendar and email search limits
  • Configuration

    • Environment variable support for Google authentication credentials and settings

qhkm and others added 8 commits February 26, 2026 04:12
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add GoogleTool struct implementing the Tool trait with actions for
Gmail (search, read, send, reply) and Calendar (list, create, freebusy).
Feature-gated behind --features google; all imports use gogcli-rs crate
functions. Includes 20 unit tests covering param validation, action
dispatch, and dangerous action detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

This PR integrates Google Workspace capabilities (Gmail and Calendar) into ZeptoClaw via the gogcli-rs crates. A new feature-gated GoogleTool provides seven actions: gmail search, read, send, reply, calendar listing, creation, and free/busy checking. Integration includes OAuth provider configuration, token management, configuration schema, CLI registration, and safety controls.

Changes

Cohort / File(s) Summary
Dependencies & Module Structure
Cargo.toml, src/lib.rs, src/tools/mod.rs
Added four optional git dependencies (gog-gmail, gog-calendar, gog-auth, gog-core) with a new google feature. Conditionally exported GoogleTool from library and tools module behind feature gate.
Google Tool Implementation
src/tools/google.rs
New 804-line GoogleTool module with constructor, metadata methods, and Tool trait implementation. Supports seven actions (gmail\_search, gmail\_read, gmail\_send, gmail\_reply, calendar\_list, calendar\_create, calendar\_freebusy) with input validation, error handling, and comprehensive unit tests.
Authentication & Configuration
src/auth/mod.rs, src/config/types.rs, src/config/mod.rs
Added Google OAuth provider configuration with scopes. Introduced GoogleToolConfig struct with access\_token, client\_id, client\_secret, default\_calendar, and max\_search\_results. Added environment variable overrides for Google credentials.
CLI & Tool Registration
src/cli/common.rs, src/cli/tools.rs
Added token resolution helper (resolve\_google\_token) to retrieve tokens from file or config. Registered GoogleTool during agent initialization when enabled and token available. Added tool listing entry and configuration detection for "google" tool.
Safety Controls
src/tools/approval.rs
Added "google" to default dangerous tools list, requiring approval before execution.

Sequence Diagram

sequenceDiagram
    participant Agent
    participant CLI
    participant Config
    participant TokenFile as Token Storage
    participant GoogleTool
    participant Gmail/Calendar as Gmail/Calendar APIs

    Agent->>CLI: Initialize with google feature
    CLI->>Config: Load config
    Config-->>CLI: config loaded
    CLI->>TokenFile: resolve_google_token()
    alt Token file exists and valid
        TokenFile-->>CLI: access_token
    else Token expired or missing
        Config-->>CLI: fallback to config access_token
    end
    CLI->>GoogleTool: new(access_token, default_calendar, max_results)
    GoogleTool-->>CLI: GoogleTool instance
    CLI->>Agent: Register GoogleTool
    Agent->>GoogleTool: execute(action, params)
    GoogleTool->>Gmail/Calendar: API request (gmail_search, calendar_create, etc.)
    Gmail/Calendar-->>GoogleTool: Response
    GoogleTool-->>Agent: ToolOutput (results or error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 hops excitedly
Gmail and Calendar aligned,
Feature-gated, smartly designed,
Inbox tamed with fuzzy cheer,
Agents with no inbox fear! 📧📅

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Google Workspace tools via gogcli-rs (Gmail + Calendar)' directly and clearly summarizes the main addition: integrating Google Workspace (Gmail and Calendar) tools via the gogcli-rs library.
Linked Issues check ✅ Passed All Phase 1 core tool requirements from issue #160 are implemented: GoogleTool with seven actions (gmail_search, gmail_read, gmail_send, gmail_reply, calendar_list, calendar_create, calendar_freebusy), OAuth auth integration, config support, and ApprovalGate safety for write actions.
Out of Scope Changes check ✅ Passed All changes directly support the Phase 1 implementation scope from issue #160; no out-of-scope additions detected. The approval defaults update correctly marks the google tool as dangerous for safety.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/google-workspace-tools

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (1)
src/config/mod.rs (1)

573-582: Add env overrides for the remaining Google tool fields.

default_calendar and max_search_results exist in GoogleToolConfig but aren’t overridable via env yet. Adding them would complete env parity for this new config block.

Proposed patch
         if let Ok(val) = std::env::var("ZEPTOCLAW_TOOLS_GOOGLE_CLIENT_SECRET") {
             self.tools.google.client_secret = Some(val);
         }
+        if let Ok(val) = std::env::var("ZEPTOCLAW_TOOLS_GOOGLE_DEFAULT_CALENDAR") {
+            if !val.trim().is_empty() {
+                self.tools.google.default_calendar = val;
+            }
+        }
+        if let Ok(val) = std::env::var("ZEPTOCLAW_TOOLS_GOOGLE_MAX_SEARCH_RESULTS") {
+            if let Ok(v) = val.parse::<u32>() {
+                self.tools.google.max_search_results = v.clamp(1, 100);
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/config/mod.rs` around lines 573 - 582, Add env overrides for
GoogleToolConfig's default_calendar and max_search_results by reading
ZEPTOCLAW_TOOLS_GOOGLE_DEFAULT_CALENDAR and
ZEPTOCLAW_TOOLS_GOOGLE_MAX_SEARCH_RESULTS in the same block that sets
access_token/client_id/client_secret; set self.tools.google.default_calendar =
Some(val) for the calendar and parse the max_search_results string to an integer
(handle parse errors by ignoring or logging) then assign to
self.tools.google.max_search_results (or Some(parsed) depending on the field
type) so both fields have env-based overrides like the others.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Cargo.toml`:
- Around line 183-186: The git dependencies gog-gmail, gog-calendar, gog-auth,
and gog-core in Cargo.toml currently use floating refs; update each dependency
entry to pin to an immutable revision by adding a specific rev (commit SHA) or a
fixed tag (e.g., replace the current { git = "...", optional = true } with { git
= "...", rev = "<commit-sha>", optional = true } or use tag = "<tag>" ),
ensuring each of gog-gmail, gog-calendar, gog-auth, and gog-core references a
concrete immutable revision to make builds deterministic.

In `@src/auth/mod.rs`:
- Around line 193-196: Replace the overly-broad Google OAuth scopes declared in
the scopes vector in src/auth/mod.rs: instead of
"https://www.googleapis.com/auth/gmail.modify" and
"https://www.googleapis.com/auth/calendar", update the scopes vector used by the
authentication code to the least-privilege alternatives: add
"https://www.googleapis.com/auth/gmail.readonly" and
"https://www.googleapis.com/auth/gmail.send" for Gmail, and add
"https://www.googleapis.com/auth/calendar.freebusy",
"https://www.googleapis.com/auth/calendar.events.readonly", and
"https://www.googleapis.com/auth/calendar.events" for Calendar; ensure these new
scope strings replace the original entries in the scopes variable so the auth
flow requests only the necessary permissions.

In `@src/cli/common.rs`:
- Around line 1299-1321: The resolve_google_token function currently reads
tokens/google.json directly and bypasses the encrypted TokenStore and refresh
logic; replace the raw tokio::fs::read_to_string usage inside
resolve_google_token with the TokenStore API (use the project's
TokenStore/zeptoclaw::auth token store loader) so the code loads tokens via the
store (which handles decryption) and invokes the store/SDK refresh path when
necessary, then return the access_token from the resulting OAuthTokenSet (or
fall back to config.tools.google.access_token as before). Ensure you reference
resolve_google_token, zeptoclaw::auth::OAuthTokenSet, and the project TokenStore
methods when making the change so registration/refresh flows are preserved.

In `@src/cli/tools.rs`:
- Around line 227-240: The "google" readiness check in src/cli/tools.rs
currently treats a non-empty client_id as sufficient; change it so only a
present non-empty access_token marks Google as ready. Update the match arm that
references config.tools.google.access_token and config.tools.google.client_id to
remove (or disable) the client_id clause and only return true when
config.tools.google.access_token.as_ref().is_some_and(|v| !v.trim().is_empty()).
Keep the rest of the surrounding logic unchanged so other providers are
unaffected.
- Around line 119-124: The google ToolInfo entry is currently always included
even when the crate is built without the google feature; wrap the ToolInfo {
name: "google", ... } entry in a feature gate (e.g., #[cfg(feature = "google")]
or push it conditionally) so it is only compiled when the "google" feature is
enabled, and replace the hardcoded tool count (currently set to 20) with a
dynamic count derived from the actual tools collection (e.g., tools.len()) or
compute the count with the same feature-gated logic so the displayed count
matches the compiled set of tools.

In `@src/tools/google.rs`:
- Around line 161-165: Update the JSON schema description for the "attendees"
property so it correctly reflects usage: state that "attendees" is a list of
attendee email addresses for calendar_create, and that free/busy queries use the
"calendars" field (calendar IDs) instead of attendees (the code reading
"calendars" in the free/busy path should be referenced). Edit the description on
the "attendees" schema entry to explicitly mention that calendar_freebusy
expects "calendars" (calendar IDs) while "attendees" is optional and only for
calendar_create.
- Around line 176-196: In execute (async fn execute) sanitize and size-cap the
variable `output` before calling ToolOutput::llm_only: implement or call a
helper (e.g., sanitize_and_cap_output) that strips embedded base64 data URIs and
long hex blobs, removes/neutralizes binary-like sequences, and then truncates
the resulting UTF-8 string to a 50KB cap (e.g., 51200 bytes) preserving valid
UTF-8 boundaries; replace Ok(ToolOutput::llm_only(output)) with
Ok(ToolOutput::llm_only(sanitize_and_cap_output(output))) so all actions
(gmail_search, gmail_read, gmail_send, gmail_reply, calendar_*) return
sanitized, size-limited content to the model.
- Around line 32-38: The struct GoogleTool currently derives Debug which exposes
sensitive data (access_token); remove the #[derive(Debug)] annotation from the
GoogleTool declaration and, if a Debug implementation is still required
elsewhere, add a custom impl Debug for GoogleTool that redacts or omits the
access_token field (e.g., print "<redacted>" for access_token) while retaining
safe representations for client, default_calendar, and max_search_results;
update any call sites that relied on the auto-derived Debug if necessary to use
the new redacted Debug or explicit logging of non-sensitive fields.
- Around line 47-53: Replace the default reqwest client construction in the
Google client constructor: in the Google::new function where Client::new() is
used, build the client with
Client::builder().timeout(Duration::from_secs(120)).build() and assign that
client to the struct's client field instead; ensure you import
std::time::Duration and handle the Result from build() (or unwrap/expect with a
clear message) so the access_token, default_calendar, and max_search_results
assignments remain unchanged.

---

Nitpick comments:
In `@src/config/mod.rs`:
- Around line 573-582: Add env overrides for GoogleToolConfig's default_calendar
and max_search_results by reading ZEPTOCLAW_TOOLS_GOOGLE_DEFAULT_CALENDAR and
ZEPTOCLAW_TOOLS_GOOGLE_MAX_SEARCH_RESULTS in the same block that sets
access_token/client_id/client_secret; set self.tools.google.default_calendar =
Some(val) for the calendar and parse the max_search_results string to an integer
(handle parse errors by ignoring or logging) then assign to
self.tools.google.max_search_results (or Some(parsed) depending on the field
type) so both fields have env-based overrides like the others.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08bf53a and 045157f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • CLAUDE.md
  • Cargo.toml
  • src/auth/mod.rs
  • src/cli/common.rs
  • src/cli/tools.rs
  • src/config/mod.rs
  • src/config/types.rs
  • src/lib.rs
  • src/tools/approval.rs
  • src/tools/google.rs
  • src/tools/mod.rs

Comment thread Cargo.toml
Comment on lines +183 to +186
gog-gmail = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
gog-calendar = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
gog-auth = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
gog-core = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the Cargo.toml file
fd Cargo.toml -t f | head -5

Repository: qhkm/zeptoclaw

Length of output: 68


🏁 Script executed:

# Read the Cargo.toml around lines 183-186
wc -l Cargo.toml && echo "---" && sed -n '180,190p' Cargo.toml

Repository: qhkm/zeptoclaw

Length of output: 849


Pin gogcli-rs git dependencies to immutable revisions.

Lines 183-186 use moving git refs with no rev or tag, which can cause non-deterministic builds across different runs or CI environments.

🔒 Suggested hardening
-gog-gmail    = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
-gog-calendar = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
-gog-auth     = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
-gog-core     = { git = "https://github.com/qhkm/gogcli-rs", optional = true }
+gog-gmail    = { git = "https://github.com/qhkm/gogcli-rs", rev = "<commit-sha>", optional = true }
+gog-calendar = { git = "https://github.com/qhkm/gogcli-rs", rev = "<commit-sha>", optional = true }
+gog-auth     = { git = "https://github.com/qhkm/gogcli-rs", rev = "<commit-sha>", optional = true }
+gog-core     = { git = "https://github.com/qhkm/gogcli-rs", rev = "<commit-sha>", optional = true }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Cargo.toml` around lines 183 - 186, The git dependencies gog-gmail,
gog-calendar, gog-auth, and gog-core in Cargo.toml currently use floating refs;
update each dependency entry to pin to an immutable revision by adding a
specific rev (commit SHA) or a fixed tag (e.g., replace the current { git =
"...", optional = true } with { git = "...", rev = "<commit-sha>", optional =
true } or use tag = "<tag>" ), ensuring each of gog-gmail, gog-calendar,
gog-auth, and gog-core references a concrete immutable revision to make builds
deterministic.

Comment thread src/auth/mod.rs
Comment on lines +193 to +196
scopes: vec![
"https://www.googleapis.com/auth/gmail.modify".to_string(),
"https://www.googleapis.com/auth/calendar".to_string(),
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Google OAuth scopes Gmail API minimum required search read send reply

💡 Result:

Minimum Gmail API OAuth scopes by capability (principle of least privilege):

  • Search + read message content (list/search, get full messages):
    https://www.googleapis.com/auth/gmail.readonly (read all resources + metadata; no writes). [1]
    If you only need search + headers/metadata (not body/attachments), consider:
    https://www.googleapis.com/auth/gmail.metadata (headers/labels/history; no body). [1]

  • Send new email:
    https://www.googleapis.com/auth/gmail.send (send only; no read/modify). [1]

  • Reply to an email:
    API-wise, replying is still “send”, so gmail.send is the minimum to transmit the reply. [1]
    In practice, most apps also need gmail.readonly to find the message/thread and build a proper reply (search/read the original, get threadId, headers, etc.). [2]

Recommended minimal combinations:

  • Search/read + send/reply: gmail.readonly + gmail.send. [1]
  • If you also need to modify mailbox state (labels, mark read/unread, etc.): https://www.googleapis.com/auth/gmail.modify (covers read/write + send, but broader). [1]

🌐 Web query:

Google Calendar API minimum OAuth scopes freebusy list create events

💡 Result:

Minimum OAuth scopes (least privilege) for common Google Calendar API tasks:

  • Free/busy lookup (freebusy.query):
    https://www.googleapis.com/auth/calendar.freebusy (availability only). [1]

  • List events (events.list):
    https://www.googleapis.com/auth/calendar.events.readonly (read events only). [2]

  • Create events (events.insert / “create events”):
    https://www.googleapis.com/auth/calendar.events (create/edit events). [3][4]

Notes:

  • Broader alternatives that also work but grant more access include .../auth/calendar.readonly and .../auth/calendar. [1][2][3]

🌐 Web query:

site:developers.google.com Gmail API OAuth scopes gmail.modify

💡 Result:

gmail.modify is the Gmail API OAuth scope:

  • Scope URI: https://www.googleapis.com/auth/gmail.modify (developers.google.com)
  • What it grants: “Read, compose, and send emails from your Gmail account.” (developers.google.com)
  • Example API operations that accept it: endpoints that modify mailbox state (for example, users.threads.modify to add/remove labels on a thread) list gmail.modify as an allowed authorization scope. (developers.google.com)

Citations:


🌐 Web query:

site:developers.google.com Calendar API OAuth scopes minimum permissions

💡 Result:

Google’s guidance is to request the most narrowly focused Calendar scope that still enables your specific feature, and avoid broader scopes like full .../auth/calendar unless you truly need them. [1]

Common “minimum permission” choices (pick the smallest that fits)

  • Only check availability (free/busy), no event details

    • https://www.googleapis.com/auth/calendar.freebusy [1]
  • Read events (titles/times/etc.) but don’t modify

    • https://www.googleapis.com/auth/calendar.events.readonly (read events on calendars the user can access) [2]
  • Create/update/delete events (but not manage calendars themselves)

    • https://www.googleapis.com/auth/calendar.events [1]
  • Read calendar metadata / list calendars (helpful if you need to let the user pick a calendar)

    • https://www.googleapis.com/auth/calendar.calendarlist.readonly (see subscribed calendar list) [1]
    • https://www.googleapis.com/auth/calendar.calendars.readonly (read calendar properties like title/time zone) [1]
  • Read everything in Calendar (broad)

    • https://www.googleapis.com/auth/calendar.readonly [1]
  • Full control (broadest)

    • https://www.googleapis.com/auth/calendar [1]

If you tell me what your app does (e.g., “create events on a chosen calendar” vs “just show next meetings”), I can name the tightest exact set of scopes to request.

Sources: Google Calendar API “Choose Google Calendar API scopes” [1]; “OAuth 2.0 Scopes for Google APIs” (Calendar API, v3) [2].


🏁 Script executed:

cat -n src/auth/mod.rs | sed -n '180,210p'

Repository: qhkm/zeptoclaw

Length of output: 1517


🏁 Script executed:

# Search for Gmail and Calendar API usage in the codebase to understand what operations are performed
rg -n "gmail|calendar" src/ --type rs -i | head -30

Repository: qhkm/zeptoclaw

Length of output: 84


🏁 Script executed:

rg -n "gmail\|calendar" src/ -i -A 2 | head -50

Repository: qhkm/zeptoclaw

Length of output: 40


🏁 Script executed:

# Search for API client usage or method calls related to Gmail/Calendar
rg -n "messages\|threads\|events\|freebusy" src/ -i --type-add 'rs:*.rs' | head -40

Repository: qhkm/zeptoclaw

Length of output: 40


🏁 Script executed:

# Look for external API/HTTP calls that might use these scopes
rg -n "POST\|GET\|reqwest\|http" src/ -i | grep -E "(gmail|calendar|googleapis)" -i

Repository: qhkm/zeptoclaw

Length of output: 40


🏁 Script executed:

# Get broader view of repository structure
find src -name "*.rs" | head -20

Repository: qhkm/zeptoclaw

Length of output: 452


🏁 Script executed:

# Check if there are any tool definitions or agent implementations
rg -n "tool\|agent\|action" src/ --type-add 'rs:*.rs' | head -30

Repository: qhkm/zeptoclaw

Length of output: 40


🏁 Script executed:

# Look for the actual operations mentioned in the original review
rg -n "search\|send\|reply\|freebusy\|create.*event\|list.*event" src/ --type-add 'rs:*.rs' -i | head -20

Repository: qhkm/zeptoclaw

Length of output: 40


🏁 Script executed:

# Check agent module for tool definitions
head -100 src/agent/mod.rs

Repository: qhkm/zeptoclaw

Length of output: 2525


🏁 Script executed:

# Check if there are any tool definitions at all in the codebase
rg -n "struct.*Tool\|trait.*Tool\|impl.*Tool" src/ --type-add 'rs:*.rs' | head -20

Repository: qhkm/zeptoclaw

Length of output: 40


🏁 Script executed:

# Look at the tests in auth/mod.rs to see what's being tested
tail -50 src/auth/mod.rs

Repository: qhkm/zeptoclaw

Length of output: 1672


Replace Google OAuth scopes with least-privilege alternatives.

Lines 194–195 request overly broad scopes. According to Google's official API documentation, use:

  • Gmail (for search/read + send/reply): Replace gmail.modify with:

    • https://www.googleapis.com/auth/gmail.readonly (search/read messages)
    • https://www.googleapis.com/auth/gmail.send (send/reply only)
  • Calendar (for freebusy + list + create): Replace calendar with:

    • https://www.googleapis.com/auth/calendar.freebusy (freebusy queries)
    • https://www.googleapis.com/auth/calendar.events.readonly (list events)
    • https://www.googleapis.com/auth/calendar.events (create/edit events)

Google's guidance is to request the most narrowly focused scope; gmail.modify and calendar grant broader permissions than necessary and increase token blast radius.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auth/mod.rs` around lines 193 - 196, Replace the overly-broad Google
OAuth scopes declared in the scopes vector in src/auth/mod.rs: instead of
"https://www.googleapis.com/auth/gmail.modify" and
"https://www.googleapis.com/auth/calendar", update the scopes vector used by the
authentication code to the least-privilege alternatives: add
"https://www.googleapis.com/auth/gmail.readonly" and
"https://www.googleapis.com/auth/gmail.send" for Gmail, and add
"https://www.googleapis.com/auth/calendar.freebusy",
"https://www.googleapis.com/auth/calendar.events.readonly", and
"https://www.googleapis.com/auth/calendar.events" for Calendar; ensure these new
scope strings replace the original entries in the scopes variable so the auth
flow requests only the necessary permissions.

Comment thread src/cli/common.rs
Comment on lines +1299 to +1321
/// Resolve Google access token: stored OAuth -> config fallback.
#[cfg(feature = "google")]
async fn resolve_google_token(config: &Config) -> Option<String> {
// 1. Try stored OAuth token
let token_path = Config::dir().join("tokens").join("google.json");
if let Ok(data) = tokio::fs::read_to_string(&token_path).await {
if let Ok(token_set) = serde_json::from_str::<zeptoclaw::auth::OAuthTokenSet>(&data) {
if !token_set.is_expired() {
return Some(token_set.access_token.clone());
}
tracing::warn!("Stored Google OAuth token expired, falling back to config");
}
}

// 2. Fall back to static access_token from config
config
.tools
.google
.access_token
.as_deref()
.filter(|t| !t.trim().is_empty())
.map(String::from)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve Google OAuth from TokenStore instead of raw JSON file reads.

On Line 1303, token resolution reads tokens/google.json directly and only checks is_expired(). This bypasses the encrypted store/refresh path and can leave the Google tool unregistered even when a refreshable token exists (registration happens before refresh at Line 1043).

🔧 Suggested fix
 #[cfg(feature = "google")]
 async fn resolve_google_token(config: &Config) -> Option<String> {
-    // 1. Try stored OAuth token
-    let token_path = Config::dir().join("tokens").join("google.json");
-    if let Ok(data) = tokio::fs::read_to_string(&token_path).await {
-        if let Ok(token_set) = serde_json::from_str::<zeptoclaw::auth::OAuthTokenSet>(&data) {
-            if !token_set.is_expired() {
-                return Some(token_set.access_token.clone());
-            }
-            tracing::warn!("Stored Google OAuth token expired, falling back to config");
-        }
-    }
+    // 1. Try encrypted OAuth token store (and refresh if needed)
+    if let Ok(encryption) = zeptoclaw::security::encryption::resolve_master_key(false) {
+        let store = auth::store::TokenStore::new(encryption);
+        if let Ok(Some(token)) = store.load("google") {
+            if token.is_expired() {
+                let _ = auth::refresh::ensure_fresh_token(&store, "google").await;
+                if let Ok(Some(fresh)) = store.load("google") {
+                    if !fresh.is_expired() {
+                        return Some(fresh.access_token);
+                    }
+                }
+                warn!("Stored Google OAuth token expired, falling back to config");
+            } else {
+                return Some(token.access_token);
+            }
+        }
+    }
 
     // 2. Fall back to static access_token from config
     config
         .tools
         .google
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/common.rs` around lines 1299 - 1321, The resolve_google_token
function currently reads tokens/google.json directly and bypasses the encrypted
TokenStore and refresh logic; replace the raw tokio::fs::read_to_string usage
inside resolve_google_token with the TokenStore API (use the project's
TokenStore/zeptoclaw::auth token store loader) so the code loads tokens via the
store (which handles decryption) and invokes the store/SDK refresh path when
necessary, then return the access_token from the resulting OAuthTokenSet (or
fall back to config.tools.google.access_token as before). Ensure you reference
resolve_google_token, zeptoclaw::auth::OAuthTokenSet, and the project TokenStore
methods when making the change so registration/refresh flows are preserved.

Comment thread src/cli/tools.rs
Comment on lines +119 to +124
ToolInfo {
name: "google",
description: "Gmail + Calendar (search, read, send, schedule)",
requires_config: true,
config_hint: "Run `zeptoclaw auth login google` or set tools.google.access_token",
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Feature-gate the CLI tool listing entry for google.

Line 119 adds google unconditionally, so builds without --features google still display it as available. Line 277 then hardcodes the count to 20, which reinforces that mismatch.

Proposed patch
     ToolInfo {
         name: "google_sheets",
         description: "Read/write Google Sheets",
         requires_config: true,
         config_hint: "Set tools.google_sheets.access_token",
     },
+    #[cfg(feature = "google")]
     ToolInfo {
         name: "google",
         description: "Gmail + Calendar (search, read, send, schedule)",
         requires_config: true,
         config_hint: "Run `zeptoclaw auth login google` or set tools.google.access_token",
@@
     #[test]
     fn test_tools_list_count() {
+        #[cfg(feature = "google")]
         assert_eq!(TOOLS.len(), 20);
+        #[cfg(not(feature = "google"))]
+        assert_eq!(TOOLS.len(), 19);
     }

Also applies to: 277-277

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/tools.rs` around lines 119 - 124, The google ToolInfo entry is
currently always included even when the crate is built without the google
feature; wrap the ToolInfo { name: "google", ... } entry in a feature gate
(e.g., #[cfg(feature = "google")] or push it conditionally) so it is only
compiled when the "google" feature is enabled, and replace the hardcoded tool
count (currently set to 20) with a dynamic count derived from the actual tools
collection (e.g., tools.len()) or compute the count with the same feature-gated
logic so the displayed count matches the compiled set of tools.

Comment thread src/cli/tools.rs
Comment on lines +227 to +240
"google" => {
config
.tools
.google
.access_token
.as_ref()
.is_some_and(|v| !v.trim().is_empty())
|| config
.tools
.google
.client_id
.as_ref()
.is_some_and(|v| !v.trim().is_empty())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid marking google as ready when only client_id is set.

Line 227-240 currently reports configured with client_id alone, which can show false “ready” status without usable credentials/token.

Proposed patch
         "google" => {
             config
                 .tools
                 .google
                 .access_token
                 .as_ref()
                 .is_some_and(|v| !v.trim().is_empty())
                 || config
                     .tools
                     .google
                     .client_id
                     .as_ref()
                     .is_some_and(|v| !v.trim().is_empty())
+                    && config
+                        .tools
+                        .google
+                        .client_secret
+                        .as_ref()
+                        .is_some_and(|v| !v.trim().is_empty())
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/tools.rs` around lines 227 - 240, The "google" readiness check in
src/cli/tools.rs currently treats a non-empty client_id as sufficient; change it
so only a present non-empty access_token marks Google as ready. Update the match
arm that references config.tools.google.access_token and
config.tools.google.client_id to remove (or disable) the client_id clause and
only return true when config.tools.google.access_token.as_ref().is_some_and(|v|
!v.trim().is_empty()). Keep the rest of the surrounding logic unchanged so other
providers are unaffected.

Comment thread src/tools/google.rs
Comment on lines +32 to +38
#[derive(Debug)]
pub struct GoogleTool {
client: Client,
access_token: String,
default_calendar: String,
max_search_results: u32,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not derive Debug on a struct that holds OAuth tokens.

Line 35 stores a bearer token, and #[derive(Debug)] can accidentally expose it through logs or panic output.

🔐 Suggested fix
-#[derive(Debug)]
 pub struct GoogleTool {
     client: Client,
     access_token: String,
     default_calendar: String,
     max_search_results: u32,
 }
+
+impl std::fmt::Debug for GoogleTool {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("GoogleTool")
+            .field("client", &"reqwest::Client")
+            .field("access_token", &"<redacted>")
+            .field("default_calendar", &self.default_calendar)
+            .field("max_search_results", &self.max_search_results)
+            .finish()
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Debug)]
pub struct GoogleTool {
client: Client,
access_token: String,
default_calendar: String,
max_search_results: u32,
}
pub struct GoogleTool {
client: Client,
access_token: String,
default_calendar: String,
max_search_results: u32,
}
impl std::fmt::Debug for GoogleTool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GoogleTool")
.field("client", &"reqwest::Client")
.field("access_token", &"<redacted>")
.field("default_calendar", &self.default_calendar)
.field("max_search_results", &self.max_search_results)
.finish()
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/google.rs` around lines 32 - 38, The struct GoogleTool currently
derives Debug which exposes sensitive data (access_token); remove the
#[derive(Debug)] annotation from the GoogleTool declaration and, if a Debug
implementation is still required elsewhere, add a custom impl Debug for
GoogleTool that redacts or omits the access_token field (e.g., print
"<redacted>" for access_token) while retaining safe representations for client,
default_calendar, and max_search_results; update any call sites that relied on
the auto-derived Debug if necessary to use the new redacted Debug or explicit
logging of non-sensitive fields.

Comment thread src/tools/google.rs
Comment on lines +47 to +53
pub fn new(access_token: &str, default_calendar: &str, max_search_results: u32) -> Self {
Self {
client: Client::new(),
access_token: access_token.to_string(),
default_calendar: default_calendar.to_string(),
max_search_results,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/tools/google.rs | head -60

Repository: qhkm/zeptoclaw

Length of output: 2702


🏁 Script executed:

rg "Client::new()" --type rust -B 2 -A 2

Repository: qhkm/zeptoclaw

Length of output: 12946


🏁 Script executed:

grep -A 2 "timeout.*120" src/providers/openai.rs | head -10

Repository: qhkm/zeptoclaw

Length of output: 338


Set an explicit 120-second timeout on the Google HTTP client.

Line 49 uses Client::new() with default timeout. Use Client::builder().timeout(Duration::from_secs(120)).build() to match the guideline for **/*.rs: reqwest HTTP client with 120s timeout. A stuck upstream call can hang tool execution indefinitely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/google.rs` around lines 47 - 53, Replace the default reqwest client
construction in the Google client constructor: in the Google::new function where
Client::new() is used, build the client with
Client::builder().timeout(Duration::from_secs(120)).build() and assign that
client to the struct's client field instead; ensure you import
std::time::Duration and handle the Result from build() (or unwrap/expect with a
clear message) so the access_token, default_calendar, and max_search_results
assignments remain unchanged.

Comment thread src/tools/google.rs
Comment on lines +161 to +165
"attendees": {
"type": "array",
"items": { "type": "string" },
"description": "Attendee email addresses. Optional for calendar_create; optional calendar IDs for calendar_freebusy."
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the attendees schema description.

Line 164 says attendees is used for calendar_freebusy, but execution logic reads calendars for free/busy inputs (Line 484 onward). This is misleading for callers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/google.rs` around lines 161 - 165, Update the JSON schema
description for the "attendees" property so it correctly reflects usage: state
that "attendees" is a list of attendee email addresses for calendar_create, and
that free/busy queries use the "calendars" field (calendar IDs) instead of
attendees (the code reading "calendars" in the free/busy path should be
referenced). Edit the description on the "attendees" schema entry to explicitly
mention that calendar_freebusy expects "calendars" (calendar IDs) while
"attendees" is optional and only for calendar_create.

Comment thread src/tools/google.rs
Comment on lines +176 to +196
async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<ToolOutput> {
let action = args
.get("action")
.and_then(Value::as_str)
.ok_or_else(|| ZeptoError::Tool("Missing 'action' parameter".to_string()))?;

let output = match action {
"gmail_search" => self.gmail_search(&args).await?,
"gmail_read" => self.gmail_read(&args).await?,
"gmail_send" => self.gmail_send(&args, false).await?,
"gmail_reply" => self.gmail_send(&args, true).await?,
"calendar_list" => self.calendar_list(&args).await?,
"calendar_create" => self.calendar_create(&args).await?,
"calendar_freebusy" => self.calendar_freebusy(&args).await?,
other => {
return Err(ZeptoError::Tool(format!("Unknown action '{}'", other)));
}
};

Ok(ToolOutput::llm_only(output))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sanitize and size-cap tool output before returning it to the model.

Line 195 returns raw formatted API data via ToolOutput::llm_only(output). This path should enforce sanitization/truncation (base64/hex stripping + 50KB cap) before model exposure.

As per coding guidelines src/tools/**.rs: Sanitize tool results before LLM processing: strip base64 URIs, hex blobs, truncate to 50KB.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/google.rs` around lines 176 - 196, In execute (async fn execute)
sanitize and size-cap the variable `output` before calling ToolOutput::llm_only:
implement or call a helper (e.g., sanitize_and_cap_output) that strips embedded
base64 data URIs and long hex blobs, removes/neutralizes binary-like sequences,
and then truncates the resulting UTF-8 string to a 50KB cap (e.g., 51200 bytes)
preserving valid UTF-8 boundaries; replace Ok(ToolOutput::llm_only(output)) with
Ok(ToolOutput::llm_only(sanitize_and_cap_output(output))) so all actions
(gmail_search, gmail_read, gmail_send, gmail_reply, calendar_*) return
sanitized, size-limited content to the model.

@qhkm qhkm merged commit 44484fa into main Feb 26, 2026
5 of 8 checks passed
@qhkm qhkm deleted the feat/google-workspace-tools branch February 26, 2026 06:09
qhkm added a commit that referenced this pull request Feb 26, 2026
Adds control panel (#180), Google Workspace tools (#162),
per-skill install fix (#169), and fallback model mapping fix (#166).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Google Workspace tools via gogcli-rs (Gmail + Calendar)

1 participant