feat: Google Workspace tools via gogcli-rs (Gmail + Calendar)#162
Conversation
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>
📝 WalkthroughWalkthroughThis PR integrates Google Workspace capabilities (Gmail and Calendar) into ZeptoClaw via the gogcli-rs crates. A new feature-gated Changes
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (1)
src/config/mod.rs (1)
573-582: Add env overrides for the remaining Google tool fields.
default_calendarandmax_search_resultsexist inGoogleToolConfigbut 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (11)
CLAUDE.mdCargo.tomlsrc/auth/mod.rssrc/cli/common.rssrc/cli/tools.rssrc/config/mod.rssrc/config/types.rssrc/lib.rssrc/tools/approval.rssrc/tools/google.rssrc/tools/mod.rs
| 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 } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and examine the Cargo.toml file
fd Cargo.toml -t f | head -5Repository: 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.tomlRepository: 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.
| scopes: vec![ | ||
| "https://www.googleapis.com/auth/gmail.modify".to_string(), | ||
| "https://www.googleapis.com/auth/calendar".to_string(), | ||
| ], |
There was a problem hiding this comment.
🧩 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”, sogmail.sendis the minimum to transmit the reply. [1]
In practice, most apps also needgmail.readonlyto find the message/thread and build a proper reply (search/read the original, getthreadId, 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.readonlyand.../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.modifyto add/remove labels on a thread) listgmail.modifyas an allowed authorization scope. (developers.google.com)
Citations:
- 1: https://developers.google.com/identity/protocols/oauth2/scopes?utm_source=openai
- 2: https://developers.google.com/identity/protocols/oauth2/scopes?utm_source=openai
- 3: https://developers.google.com/gmail/api/reference/rest/v1/users.threads/modify?utm_source=openai
🌐 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 -30Repository: qhkm/zeptoclaw
Length of output: 84
🏁 Script executed:
rg -n "gmail\|calendar" src/ -i -A 2 | head -50Repository: 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 -40Repository: 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)" -iRepository: qhkm/zeptoclaw
Length of output: 40
🏁 Script executed:
# Get broader view of repository structure
find src -name "*.rs" | head -20Repository: 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 -30Repository: 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 -20Repository: qhkm/zeptoclaw
Length of output: 40
🏁 Script executed:
# Check agent module for tool definitions
head -100 src/agent/mod.rsRepository: 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 -20Repository: 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.rsRepository: 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.modifywith: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
calendarwith: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.
| /// 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 | ||
| .access_token | ||
| .as_deref() | ||
| .filter(|t| !t.trim().is_empty()) | ||
| .map(String::from) | ||
| } |
There was a problem hiding this comment.
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.
| 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", | ||
| }, |
There was a problem hiding this comment.
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.
| "google" => { | ||
| config | ||
| .tools | ||
| .access_token | ||
| .as_ref() | ||
| .is_some_and(|v| !v.trim().is_empty()) | ||
| || config | ||
| .tools | ||
| .client_id | ||
| .as_ref() | ||
| .is_some_and(|v| !v.trim().is_empty()) | ||
| } |
There was a problem hiding this comment.
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.
| #[derive(Debug)] | ||
| pub struct GoogleTool { | ||
| client: Client, | ||
| access_token: String, | ||
| default_calendar: String, | ||
| max_search_results: u32, | ||
| } |
There was a problem hiding this comment.
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.
| #[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.
| 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, | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/tools/google.rs | head -60Repository: qhkm/zeptoclaw
Length of output: 2702
🏁 Script executed:
rg "Client::new()" --type rust -B 2 -A 2Repository: qhkm/zeptoclaw
Length of output: 12946
🏁 Script executed:
grep -A 2 "timeout.*120" src/providers/openai.rs | head -10Repository: 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.
| "attendees": { | ||
| "type": "array", | ||
| "items": { "type": "string" }, | ||
| "description": "Attendee email addresses. Optional for calendar_create; optional calendar IDs for calendar_freebusy." | ||
| }, |
There was a problem hiding this comment.
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.
| 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)) | ||
| } |
There was a problem hiding this comment.
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.
Summary
qhkm/gogcli-rscrates--features google— default binary unchangedgoogletool with 7 actions:gmail_search,gmail_read,gmail_send,gmail_reply,calendar_list,calendar_create,calendar_freebusyaccess_tokengmail_send,gmail_reply,calendar_create) require ApprovalGate confirmationCloses #160
Changes
Cargo.tomlgooglefeature + gogcli-rs git depssrc/tools/google.rsGoogleToolwith 7 actions (22 unit tests)src/tools/mod.rssrc/config/types.rsGoogleToolConfigstruct +ToolsConfigfieldsrc/config/mod.rsZEPTOCLAW_TOOLS_GOOGLE_*src/cli/common.rsresolve_google_token()helpersrc/auth/mod.rssrc/cli/tools.rssrc/tools/approval.rssrc/lib.rsCLAUDE.mdTest plan
cargo fmt -- --checkcleancargo clippy --features google -- -D warnings— 0 warningscargo test --lib --features google— 2603 passed, 0 failedcargo check) unaffected — no binary size changezeptoclaw auth login google→ OAuth flowzeptoclaw agent --features google -m "search my inbox"→ tool invoked🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Configuration