Skip to content

Fix all code#730

Open
samhiotisiddn-jpg wants to merge 3 commits intogoogleworkspace:chore/sync-skillsfrom
samhiotisiddn-jpg:fix-all-code
Open

Fix all code#730
samhiotisiddn-jpg wants to merge 3 commits intogoogleworkspace:chore/sync-skillsfrom
samhiotisiddn-jpg:fix-all-code

Conversation

@samhiotisiddn-jpg
Copy link
Copy Markdown

Description

Please include a summary of the change and which issue is fixed. If adding a new feature or command, please include the output of running it with --dry-run to prove the JSON request body matches the Discovery Document schema.

Dry Run Output:

// Paste --dry-run output here if applicable

Checklist:

  • My code follows the AGENTS.md guidelines (no generated google-* crates).
  • I have run cargo fmt --all to format the code perfectly.
  • I have run cargo clippy -- -D warnings and resolved all warnings.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have provided a Changeset file (e.g. via pnpx changeset) to document my changes.

samhiotisiddn-jpg and others added 3 commits April 11, 2026 07:57
This workflow runs tests and publishes a Node.js package to GitHub Packages upon release creation.
Fixed 11 clippy warnings, ran fmt, and passed all tests.
Add top-level agent command with various tools and update workspace dependencies.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 17, 2026

⚠️ No Changeset found

Latest commit: 36b2535

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@googleworkspace-bot googleworkspace-bot added area: auth area: distribution area: http area: core Core CLI parsing, commands, error handling, utilities labels Apr 17, 2026
@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 17, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a powerful new AI agent capability to the Google Workspace CLI. By integrating with LLM providers, the CLI can now perform complex, multi-step tasks by leveraging a suite of tools, including direct Google Workspace API calls, Notion integration, Zapier automation, and browser-based actions. The implementation includes a robust configuration system, persistent memory management, and a flexible tool registry, significantly expanding the utility of the CLI for automated workflows.

Highlights

  • New Agent Command: Added a new gws agent command that enables a terminal-based AI assistant capable of tool use.
  • Tooling Infrastructure: Implemented a modular tool registry supporting Google Workspace, Notion, Zapier, Supabase, and a headless browser.
  • LLM Integration: Added an OpenAI-compatible client supporting both OpenRouter and local Ollama providers.
  • Conversational Memory: Introduced persistent memory support using Supabase or an in-process buffer for multi-turn interactions.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/npm-publish-github-packages.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a terminal-based AI agent, gws agent, which supports multi-turn conversations using LLM providers like OpenRouter and Ollama. The implementation includes a tool registry with integrations for Google Workspace, Notion, Zapier, Supabase, and a headless browser, along with persistent memory options. Feedback highlights a resource leak in the browser tool where tabs are not closed, a bypassable recursion check in the self-hosted CLI tool, and potential query parsing issues in the Supabase tool due to overly permissive URL encoding.

Comment on lines +72 to +132
fn run_blocking(
browser: &headless_chrome::Browser,
action: &str,
args: &Value,
) -> Result<String, ToolError> {
let tab = browser
.new_tab()
.map_err(|e| ToolError::runtime(format!("new_tab: {e}")))?;
match action {
"navigate" => {
let url = args
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::runtime("missing 'url'"))?;
tab.navigate_to(url)
.map_err(|e| ToolError::runtime(format!("navigate: {e}")))?;
tab.wait_until_navigated()
.map_err(|e| ToolError::runtime(format!("wait: {e}")))?;
Ok(format!("navigated to {url}"))
}
"click" => {
let url = args.get("url").and_then(|v| v.as_str());
let selector = args
.get("selector")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::runtime("missing 'selector'"))?;
if let Some(u) = url {
tab.navigate_to(u)
.map_err(|e| ToolError::runtime(format!("navigate: {e}")))?;
tab.wait_until_navigated()
.map_err(|e| ToolError::runtime(format!("wait: {e}")))?;
}
let el = tab
.wait_for_element(selector)
.map_err(|e| ToolError::runtime(format!("element: {e}")))?;
el.click()
.map_err(|e| ToolError::runtime(format!("click: {e}")))?;
Ok(format!("clicked {selector}"))
}
"text" => {
let url = args
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::runtime("missing 'url'"))?;
tab.navigate_to(url)
.map_err(|e| ToolError::runtime(format!("navigate: {e}")))?;
tab.wait_until_navigated()
.map_err(|e| ToolError::runtime(format!("wait: {e}")))?;
let body = tab
.wait_for_element("body")
.map_err(|e| ToolError::runtime(format!("body: {e}")))?;
let text = body
.get_inner_text()
.map_err(|e| ToolError::runtime(format!("inner text: {e}")))?;
Ok(truncate(&text, 4000))
}
other => Err(ToolError::runtime(format!(
"unknown browser action '{other}'"
))),
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The run_blocking function opens a new browser tab for every tool call but never closes it. Since the browser instance is reused across the entire agent session, this will lead to a significant accumulation of open tabs, consuming excessive memory and potentially causing the browser process to crash or become unresponsive during long sessions.

    fn run_blocking(
        browser: &headless_chrome::Browser,
        action: &str,
        args: &Value,
    ) -> Result<String, ToolError> {
        let tab = browser
            .new_tab()
            .map_err(|e| ToolError::runtime(format!("new_tab: {e}")))?;
        let res = match action {
            "navigate" => {
                let url = args
                    .get("url")
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| ToolError::runtime("missing 'url'"))?;
                tab.navigate_to(url)
                    .map_err(|e| ToolError::runtime(format!("navigate: {e}")))?;
                tab.wait_until_navigated()
                    .map_err(|e| ToolError::runtime(format!("wait: {e}")))?;
                Ok(format!("navigated to {url}"))
            }
            "click" => {
                let url = args.get("url").and_then(|v| v.as_str());
                let selector = args
                    .get("selector")
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| ToolError::runtime("missing 'selector'"))?;
                if let Some(u) = url {
                    tab.navigate_to(u)
                        .map_err(|e| ToolError::runtime(format!("navigate: {e}")))?;
                    tab.wait_until_navigated()
                        .map_err(|e| ToolError::runtime(format!("wait: {e}")))?;
                }
                let el = tab
                    .wait_for_element(selector)
                    .map_err(|e| ToolError::runtime(format!("element: {e}")))?;
                el.click()
                    .map_err(|e| ToolError::runtime(format!("click: {e}")))?;
                Ok(format!("clicked {selector}"))
            }
            "text" => {
                let url = args
                    .get("url")
                    .and_then(|v| v.as_str())
                    .ok_or_else(|| ToolError::runtime("missing 'url'"))?;
                tab.navigate_to(url)
                    .map_err(|e| ToolError::runtime(format!("navigate: {e}")))?;
                tab.wait_until_navigated()
                    .map_err(|e| ToolError::runtime(format!("wait: {e}")))?;
                let body = tab
                    .wait_for_element("body")
                    .map_err(|e| ToolError::runtime(format!("body: {e}")))?;
                let text = body
                    .get_inner_text()
                    .map_err(|e| ToolError::runtime(format!("inner text: {e}")))?;
                Ok(truncate(&text, 4000))
            }
            other => Err(ToolError::runtime(format!(
                "unknown browser action '{other}'"
            ))),
        };
        let _ = tab.close();
        res
    }

argv.push(s.to_string());
}
// Disallow recursive agent self-invocation to avoid runaway loops.
if argv.first().map(|s| s.as_str()) == Some("agent") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The recursion check only inspects the first argument in the vector. This can be easily bypassed by an LLM using global flags (e.g., gws --api-version v1 agent ...), leading to infinite recursive agent loops and local resource exhaustion. The check should identify the "agent" command regardless of preceding flags.

Suggested change
if argv.first().map(|s| s.as_str()) == Some("agent") {
if argv.iter().find(|s| !s.starts_with('-')).map(|s| s.as_str()) == Some("agent") {

fn encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~' | b',' | b':') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The encode function includes , and : in its allow-list for PostgREST query values. However, PostgREST uses these characters as delimiters for operators (like in) and complex types (like ranges). Allowing them unencoded in the value part can lead to ambiguous or incorrect query parsing. Following standard percent-encoding for values is safer.

Suggested change
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~' | b',' | b':') {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {

@googleworkspace-bot googleworkspace-bot force-pushed the chore/sync-skills branch 19 times, most recently from 06b6a7c to 1fc8f04 Compare April 22, 2026 07:35
@googleworkspace-bot googleworkspace-bot force-pushed the chore/sync-skills branch 4 times, most recently from f9b6687 to 57d1bb2 Compare April 22, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth area: core Core CLI parsing, commands, error handling, utilities area: distribution area: http

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants