From 4e7a572497ae05ce2fca84f9760397a0ea1f8544 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 19 Mar 2026 10:25:25 +1100 Subject: [PATCH 1/6] fix(openai): use Responses API for gpt-5.4 (#7982) Signed-off-by: Michael Neale --- crates/goose/src/providers/openai.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index ec0be3bbd1..3e032c40a3 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -233,6 +233,7 @@ impl OpenAiProvider { let normalized_model = model_name.to_ascii_lowercase(); (normalized_model.starts_with("gpt-5") && normalized_model.contains("codex")) || normalized_model.starts_with("gpt-5.2-pro") + || normalized_model.starts_with("gpt-5.4") } fn should_use_responses_api(model_name: &str, base_path: &str) -> bool { @@ -728,6 +729,22 @@ mod tests { )); } + #[test] + fn gpt_5_4_uses_responses_when_base_path_is_default() { + assert!(OpenAiProvider::should_use_responses_api( + "gpt-5.4", + "v1/chat/completions" + )); + } + + #[test] + fn gpt_5_4_with_date_uses_responses() { + assert!(OpenAiProvider::should_use_responses_api( + "gpt-5.4-2026-03-01", + "v1/chat/completions" + )); + } + #[test] fn gpt_4o_does_not_use_responses() { assert!(!OpenAiProvider::should_use_responses_api( From 5be96c1ab2e0d787236cf2434a4480f63990a585 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 19 Mar 2026 13:20:45 +1100 Subject: [PATCH 2/6] feat: add gemini-acp provider, update docs on subscription models + improvements to codex (#8000) Signed-off-by: Michael Neale --- crates/goose/src/providers/chatgpt_codex.rs | 18 +++- crates/goose/src/providers/claude_code.rs | 2 +- crates/goose/src/providers/codex.rs | 2 +- crates/goose/src/providers/gemini_acp.rs | 81 ++++++++++++++++++ crates/goose/src/providers/gemini_cli.rs | 2 +- crates/goose/src/providers/init.rs | 2 + crates/goose/src/providers/mod.rs | 1 + crates/goose/tests/providers.rs | 9 ++ .../index.md | 74 +++++++++++++++++ .../docs/getting-started/providers.md | 10 +-- documentation/docs/guides/acp-providers.md | 82 ++++++++++++++++++- documentation/docs/guides/cli-providers.md | 4 + 12 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 crates/goose/src/providers/gemini_acp.rs create mode 100644 documentation/blog/2026-03-19-use-goose-with-your-ai-subscription/index.md diff --git a/crates/goose/src/providers/chatgpt_codex.rs b/crates/goose/src/providers/chatgpt_codex.rs index 2508193dcb..1c99ad7d30 100644 --- a/crates/goose/src/providers/chatgpt_codex.rs +++ b/crates/goose/src/providers/chatgpt_codex.rs @@ -45,8 +45,9 @@ const OAUTH_TIMEOUT_SECS: u64 = 300; const HTML_AUTO_CLOSE_TIMEOUT_MS: u64 = 2000; const CHATGPT_CODEX_PROVIDER_NAME: &str = "chatgpt_codex"; -pub const CHATGPT_CODEX_DEFAULT_MODEL: &str = "gpt-5.1-codex"; +pub const CHATGPT_CODEX_DEFAULT_MODEL: &str = "gpt-5.3-codex"; pub const CHATGPT_CODEX_KNOWN_MODELS: &[&str] = &[ + "gpt-5.4", "gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.1-codex", @@ -56,6 +57,14 @@ pub const CHATGPT_CODEX_KNOWN_MODELS: &[&str] = &[ const CHATGPT_CODEX_DOC_URL: &str = "https://openai.com/chatgpt"; +const GPT_53_CODEX_TOOL_PREAMBLE: &str = "\ +You are a coding agent. You have access to tools to accomplish tasks. \ +Always use your tools to fulfill requests - do not just describe what you would do. \ +Keep going until the query is completely resolved before yielding back to the user. \ +Autonomously resolve the query using the tools available to you. \ +Do NOT guess or make up an answer. \ +Before making tool calls, send a brief message explaining what you're about to do."; + #[derive(Debug)] struct ChatGptCodexAuthState { oauth_mutex: TokioMutex<()>, @@ -181,11 +190,16 @@ fn create_codex_request( ) -> Result { let input_items = build_input_items(messages)?; + let instructions = match model_config.model_name.as_str() { + "gpt-5.3-codex" => format!("{GPT_53_CODEX_TOOL_PREAMBLE}\n\n{system}"), + _ => system.to_string(), + }; + let mut payload = json!({ "model": model_config.model_name, "input": input_items, "store": false, - "instructions": system, + "instructions": instructions, }); let payload_obj = payload diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 63c077a189..ecdb44bb4f 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -589,7 +589,7 @@ impl ProviderDef for ClaudeCodeProvider { ProviderMetadata::new( CLAUDE_CODE_PROVIDER_NAME, "Claude Code CLI", - "Requires claude CLI installed, no MCPs. Use Anthropic provider for full features.", + "[Deprecated: use claude-acp instead] Requires claude CLI installed, no MCPs. Use claude-acp for ACP support with extensions.", CLAUDE_CODE_DEFAULT_MODEL, // Only a few agentic choices; fetched dynamically via fetch_supported_models. vec![], diff --git a/crates/goose/src/providers/codex.rs b/crates/goose/src/providers/codex.rs index ab6208b62e..d121097c69 100644 --- a/crates/goose/src/providers/codex.rs +++ b/crates/goose/src/providers/codex.rs @@ -598,7 +598,7 @@ impl ProviderDef for CodexProvider { ProviderMetadata::new( CODEX_PROVIDER_NAME, "OpenAI Codex CLI", - "Execute OpenAI models via Codex CLI tool. Requires codex CLI installed.", + "[Deprecated: use chatgpt_codex or codex-acp instead] Execute OpenAI models via Codex CLI tool. Requires codex CLI installed.", CODEX_DEFAULT_MODEL, CODEX_KNOWN_MODELS.to_vec(), CODEX_DOC_URL, diff --git a/crates/goose/src/providers/gemini_acp.rs b/crates/goose/src/providers/gemini_acp.rs new file mode 100644 index 0000000000..15c3d5dc9b --- /dev/null +++ b/crates/goose/src/providers/gemini_acp.rs @@ -0,0 +1,81 @@ +use anyhow::Result; +use futures::future::BoxFuture; +use std::path::PathBuf; + +use crate::acp::{ + extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, PermissionMapping, +}; +use crate::config::search_path::SearchPaths; +use crate::config::{Config, GooseMode}; +use crate::model::ModelConfig; +use crate::providers::base::{ProviderDef, ProviderMetadata}; + +const GEMINI_ACP_PROVIDER_NAME: &str = "gemini-acp"; +pub const GEMINI_ACP_DEFAULT_MODEL: &str = "default"; +const GEMINI_ACP_DOC_URL: &str = "https://github.com/google-gemini/gemini-cli"; + +pub struct GeminiAcpProvider; + +impl ProviderDef for GeminiAcpProvider { + type Provider = AcpProvider; + + fn metadata() -> ProviderMetadata { + ProviderMetadata::new( + GEMINI_ACP_PROVIDER_NAME, + "Gemini CLI (ACP)", + "ACP provider for Google's Gemini CLI. Install: npm install -g @google/gemini-cli", + GEMINI_ACP_DEFAULT_MODEL, + vec![], + GEMINI_ACP_DOC_URL, + vec![], + ) + } + + fn from_env( + model: ModelConfig, + extensions: Vec, + ) -> BoxFuture<'static, Result> { + Box::pin(async move { + let config = Config::global(); + let command_name: String = config.get_gemini_cli_command().unwrap_or_default().into(); + let resolved_command = SearchPaths::builder().with_npm().resolve(&command_name)?; + let goose_mode = config.get_goose_mode().unwrap_or(GooseMode::Auto); + + let permission_mapping = PermissionMapping { + allow_option_id: Some("allow".to_string()), + reject_option_id: Some("reject".to_string()), + rejected_tool_status: sacp::schema::ToolCallStatus::Failed, + }; + + let mut args = vec!["--acp".to_string()]; + if model.model_name != "default" { + args.push("--model".to_string()); + args.push(model.model_name.clone()); + } + + let provider_config = AcpProviderConfig { + command: resolved_command, + args, + env: vec![], + env_remove: vec![], + work_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), + mcp_servers: extension_configs_to_mcp_servers(&extensions), + session_mode_id: Some(map_goose_mode(goose_mode)), + permission_mapping, + notification_callback: None, + }; + + let metadata = Self::metadata(); + AcpProvider::connect(metadata.name, model, goose_mode, provider_config).await + }) + } +} + +fn map_goose_mode(goose_mode: GooseMode) -> String { + match goose_mode { + GooseMode::Auto => "yolo".to_string(), + GooseMode::Approve => "default".to_string(), + GooseMode::SmartApprove => "auto_edit".to_string(), + GooseMode::Chat => "plan".to_string(), + } +} diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index a5e655984f..d892db87eb 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -161,7 +161,7 @@ impl ProviderDef for GeminiCliProvider { ProviderMetadata::new( GEMINI_CLI_PROVIDER_NAME, "Gemini CLI", - "Execute Gemini models via gemini CLI tool", + "[Deprecated: use gemini-acp instead] Execute Gemini models via gemini CLI tool. Requires gemini CLI installed.", GEMINI_CLI_DEFAULT_MODEL, GEMINI_CLI_KNOWN_MODELS.to_vec(), GEMINI_CLI_DOC_URL, diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index 1d00a9c9e0..4c02ba7406 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -14,6 +14,7 @@ use super::{ cursor_agent::CursorAgentProvider, databricks::DatabricksProvider, gcpvertexai::GcpVertexAIProvider, + gemini_acp::GeminiAcpProvider, gemini_cli::GeminiCliProvider, githubcopilot::GithubCopilotProvider, google::GoogleProvider, @@ -51,6 +52,7 @@ async fn init_registry() -> RwLock { registry.register::(false); registry.register::(true); registry.register::(false); + registry.register::(false); registry.register::(true); registry.register::(false); registry.register::(true); diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index b1d51b3de0..b1b564f4fa 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -21,6 +21,7 @@ pub mod errors; pub mod formats; mod gcpauth; pub mod gcpvertexai; +pub mod gemini_acp; pub mod gemini_cli; pub mod githubcopilot; pub mod google; diff --git a/crates/goose/tests/providers.rs b/crates/goose/tests/providers.rs index f456a5275f..f1b7c8655f 100644 --- a/crates/goose/tests/providers.rs +++ b/crates/goose/tests/providers.rs @@ -17,6 +17,7 @@ use goose::providers::codex_acp::CODEX_ACP_DEFAULT_MODEL; use goose::providers::create_with_named_model; use goose::providers::databricks::DATABRICKS_DEFAULT_MODEL; use goose::providers::errors::ProviderError; +use goose::providers::gemini_acp::GEMINI_ACP_DEFAULT_MODEL; use goose::providers::google::GOOGLE_DEFAULT_MODEL; use goose::providers::litellm::LITELLM_DEFAULT_MODEL; use goose::providers::openai::OPEN_AI_DEFAULT_MODEL; @@ -890,6 +891,14 @@ async fn test_codex_acp_provider() -> Result<()> { .await } +// Requires: npm install -g @google/gemini-cli +#[tokio::test] +async fn test_gemini_acp_provider() -> Result<()> { + ProviderTestConfig::with_agentic_provider("gemini-acp", GEMINI_ACP_DEFAULT_MODEL, "gemini") + .run() + .await +} + #[ctor::dtor] fn print_test_report() { TEST_REPORT.print_summary(); diff --git a/documentation/blog/2026-03-19-use-goose-with-your-ai-subscription/index.md b/documentation/blog/2026-03-19-use-goose-with-your-ai-subscription/index.md new file mode 100644 index 0000000000..17a0670742 --- /dev/null +++ b/documentation/blog/2026-03-19-use-goose-with-your-ai-subscription/index.md @@ -0,0 +1,74 @@ +--- +title: "Use Goose with Your AI Subscription" +description: "A quick update on using subscriptions for claude, gemini and codex" +authors: + - mic +--- + +You can use your subscriptions for codex, claude and gemini now with goose, thanks to ACP! (Agent Client Protocol). +Codex is also special in that you can login directly to chatgpt - nothing else needs to be installed. + +Gemini natively supports ACP, so it now works with a gemini acp provider in goose. At the time of writing, claude requires just one utility installed just once. + + + +## Why subscriptions? + +Well you can use what you already pay for. Obviously! and sessions and so on are still in goose. +ACP gives a deeper connection to these agents than using the CLI as providers. In this world - you can think of this as a stack of agents: +goose plugs into gemini via ACP (and other things, clients could plug in to goose!) but gemini (and also claude code) also act as an agent loop somewhat. +With ACP you are using the tools that are (mostly) in the underlying agent. Codex, however, is a full power LLM api, so you can use extensions natively in goose for that one. + +## Claude Code — via ACP + +If you have a Claude Code subscription, you can use it through goose via the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/). This requires installing a small adapter package: + +```bash +npm install -g @zed-industries/claude-agent-acp +``` + +Then configure goose to use it via the claude acp extension (CLI or GUI) + + +Or set it via environment variables: + +```bash +export GOOSE_PROVIDER=claude-acp +goose +``` + +goose passes your MCP extensions through to Claude via ACP, so any custom MCP servers you've configured in goose are available to the agent. + +## ChatGPT — sign in with your account + +If you have ChatGPT Plus or Pro, the `chatgpt_codex` provider lets you use goose with your existing account. Just pick ChatGPT when you are setting up the goose app for the first time (or changing to that provider) + +The first time you run it, goose will open a browser window for you to sign in with your ChatGPT account. After that, your session is cached locally. + +The recommended model is `gpt-5.3-codex`, which is the default. You can also select `gpt-5.4` (OpenAI's latest omni model) or `gpt-5.2-codex` from the model picker. + +## Gemini — via ACP (native) + +If you have a Google account with Gemini access, the Gemini CLI speaks ACP natively — no separate adapter needed. Just install the Gemini CLI itself: + +```bash +npm install -g @google/gemini-cli +``` + +... and run `gemini` at least once. + +On first run, Gemini CLI will ask you to authenticate with your Google account. After that, goose passes your extensions directly through to Gemini via ACP. + +## What about the old CLI providers? + +Goose previously supported `claude-code`, `codex`, and `gemini-cli` as "pass-through" CLI providers. These will be removed soon as ACP is the future! + +## Quick reference + +| Subscription | Provider | Install | Extensions | +|---|---|---|---| +| Claude Code | `claude-acp` | `npm install -g @zed-industries/claude-agent-acp` | ✅ via MCP | +| ChatGPT Plus/Pro | `chatgpt_codex` | Nothing — OAuth sign-in | ✅ via MCP | +| Gemini | `gemini-acp` | `npm install -g @google/gemini-cli` | ✅ via MCP | + +Pick the one that matches what you're already paying for, and you're good to go. diff --git a/documentation/docs/getting-started/providers.md b/documentation/docs/getting-started/providers.md index 4a989f4cd4..433851847b 100644 --- a/documentation/docs/getting-started/providers.md +++ b/documentation/docs/getting-started/providers.md @@ -55,18 +55,9 @@ goose automatically enables Anthropic's [prompt caching](https://platform.claude ### CLI Providers -goose also supports special "pass-through" providers that work with existing CLI tools, allowing you to use your subscriptions instead of paying per token: - | Provider | Description | Requirements | |-----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Claude Code](https://www.anthropic.com/claude-code) (`claude-code`) | Uses Anthropic's Claude CLI tool with your Claude Code subscription. Provides access to Claude with 200K context limit. | Claude CLI installed and authenticated, active Claude Code subscription | -| [OpenAI Codex](https://developers.openai.com/codex/cli) (`codex`) | Uses OpenAI's Codex CLI tool with your ChatGPT Plus/Pro subscription. Provides access to GPT-5 models with up to 400K context limit. | Codex CLI installed and authenticated, active ChatGPT Plus/Pro subscription | | [Cursor Agent](https://docs.cursor.com/en/cli/overview) (`cursor-agent`) | Uses Cursor's AI CLI tool with your Cursor subscription. Provides access to GPT-5, Claude 4, and other models through the cursor-agent command-line interface. | cursor-agent CLI installed and authenticated | -| [Gemini CLI](https://ai.google.dev/gemini-api/docs) (`gemini-cli`) | Uses Google's Gemini CLI tool with your Google AI subscription. Provides access to Gemini with 1M context limit. | Gemini CLI installed and authenticated | - -:::tip CLI Providers -CLI providers are cost-effective alternatives that use your existing subscriptions. They work differently from API providers as they execute CLI commands and integrate with the tools' native capabilities. See the [CLI Providers guide](/docs/guides/cli-providers) for detailed setup instructions. -::: ### ACP Providers @@ -76,6 +67,7 @@ goose supports [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) a |-----------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Claude ACP](https://github.com/zed-industries/claude-agent-acp) (`claude-acp`) | Uses Claude Code via ACP. Passes goose extensions to the agent as MCP servers. | `npm install -g @zed-industries/claude-agent-acp`, active Claude Code subscription | | [Codex ACP](https://github.com/zed-industries/codex-acp) (`codex-acp`) | Uses OpenAI Codex via ACP. Passes goose extensions to the agent as MCP servers. | `npm install -g @zed-industries/codex-acp`, active ChatGPT Plus/Pro subscription | +| [Gemini ACP](https://github.com/google-gemini/gemini-cli) (`gemini-acp`) | Uses Google's Gemini CLI via ACP (native `--acp` support). Passes goose extensions to the agent as MCP servers. | `npm install -g @google/gemini-cli`, authenticated with Google account | :::tip ACP Providers See the [ACP Providers guide](/docs/guides/acp-providers) for detailed setup instructions. diff --git a/documentation/docs/guides/acp-providers.md b/documentation/docs/guides/acp-providers.md index a53ea95208..35c078d2fb 100644 --- a/documentation/docs/guides/acp-providers.md +++ b/documentation/docs/guides/acp-providers.md @@ -11,6 +11,10 @@ goose supports [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) a ACP providers pass goose [extensions](/docs/getting-started/using-extensions) through to the agent as MCP servers, so the agent can call your extensions directly. +:::tip Use Your Existing Subscriptions +ACP providers let you use goose with your existing Claude Code, ChatGPT Plus/Pro, or Google Gemini subscriptions — no per-token API costs. They are the recommended replacement for the deprecated [CLI providers](/docs/guides/cli-providers). +::: + :::warning Limitations - **No session fork or resume**: You can start new sessions, but `goose session resume` and `goose session fork` are not supported yet. - **ACP session ID differs from goose session ID**: Telemetry fields may not correlate across the two. @@ -20,7 +24,7 @@ ACP providers pass goose [extensions](/docs/getting-started/using-extensions) th ### Claude ACP -Wraps [claude-agent-acp](https://github.com/zed-industries/claude-agent-acp), an ACP adapter for Anthropic's Claude Code. Uses the same Claude subscription as the `claude-code` CLI provider. +Wraps [claude-agent-acp](https://github.com/zed-industries/claude-agent-acp), an ACP adapter for Anthropic's Claude Code. Uses the same Claude subscription as the deprecated `claude-code` CLI provider. **Requirements:** - Node.js and npm @@ -29,13 +33,22 @@ Wraps [claude-agent-acp](https://github.com/zed-industries/claude-agent-acp), an ### Codex ACP -Wraps [codex-acp](https://github.com/zed-industries/codex-acp), an ACP adapter for OpenAI's Codex. Uses the same ChatGPT subscription as the `codex` CLI provider. Codex's sandbox blocks network by default; goose automatically enables network access when HTTP MCP servers are configured. +Wraps [codex-acp](https://github.com/zed-industries/codex-acp), an ACP adapter for OpenAI's Codex. Uses the same ChatGPT subscription as the deprecated `codex` CLI provider. Codex's sandbox blocks network by default; goose automatically enables network access when HTTP MCP servers are configured. **Requirements:** - Node.js and npm - Active ChatGPT Plus/Pro subscription or OpenAI API credits - Authenticated with your OpenAI account (`codex` CLI working) +### Gemini ACP + +Uses Google's [Gemini CLI](https://github.com/google-gemini/gemini-cli) directly via its native `--acp` flag. No shim needed — Gemini CLI speaks ACP natively. Replaces the deprecated `gemini-cli` CLI provider. + +**Requirements:** +- Node.js and npm +- Gemini CLI installed (`npm install -g @google/gemini-cli`) +- Authenticated with your Google account (run `gemini` once to authenticate via browser) + ## Setup Instructions ### Claude ACP @@ -110,6 +123,42 @@ Wraps [codex-acp](https://github.com/zed-industries/codex-acp), an ACP adapter f │ gpt-5.2-codex ``` +### Gemini ACP + +1. **Install Gemini CLI** + + ```bash + npm install -g @google/gemini-cli + ``` + +2. **Authenticate with Google** + + Run `gemini` once and follow the browser-based authentication flow. + +3. **Configure goose** + + Set the provider environment variable: + ```bash + export GOOSE_PROVIDER=gemini-acp + ``` + + Or configure through the goose CLI using `goose configure`: + + ```bash + ┌ goose-configure + │ + ◇ What would you like to configure? + │ Configure Providers + │ + ◇ Which model provider should we use? + │ Gemini CLI (ACP) + │ + ◇ Model fetch complete + │ + ◇ Enter a model from that provider: + │ default + ``` + ## Usage Examples ### Basic Usage @@ -134,6 +183,12 @@ GOOSE_PROVIDER=codex-acp goose run \ -t 'Search for flights from BKI to SYD tomorrow' ``` +```bash +GOOSE_PROVIDER=gemini-acp goose run \ + --with-extension 'npx -y @modelcontextprotocol/server-everything' \ + -t 'Use the echo tool to say hello' +``` + ## Configuration Options ### Claude ACP Configuration @@ -185,13 +240,32 @@ See [claude-agent-acp](https://github.com/zed-industries/claude-agent-acp) for s See [codex-acp](https://github.com/zed-industries/codex-acp) for approval policy and sandbox details. +### Gemini ACP Configuration + +| Environment Variable | Description | Default | +|----------------------|---------------------|-------------| +| `GOOSE_PROVIDER` | Set to `gemini-acp` | None | +| `GOOSE_MODEL` | Model to use | `default` | +| `GOOSE_MODE` | Permission mode | `auto` | + +**Permission Modes (`GOOSE_MODE`):** + +| Mode | Gemini Mode | Behavior | +|-----------------|-------------|---------------------------------------------------------------| +| `auto` | `yolo` | Auto-approves all tool calls | +| `smart-approve` | `auto_edit` | Auto-approves file edits, prompts for other operations | +| `approve` | `default` | Prompts for all permission-required operations | +| `chat` | `plan` | Planning only, no tool execution | + +See the [Gemini CLI documentation](https://github.com/google-gemini/gemini-cli) for approval mode details. + ## Error Handling ACP providers depend on external npm packages, so ensure: -- The ACP adapter binary is installed and in your PATH (`claude-agent-acp` or `codex-acp`) +- The ACP agent binary is installed and in your PATH (`claude-agent-acp`, `codex-acp`, or `gemini`) - The underlying CLI tool is authenticated and working - Subscription limits are not exceeded - Node.js and npm are installed -If goose can't find the binary, session startup will fail with an error. Run `which claude-agent-acp` or `which codex-acp` to verify installation. +If goose can't find the binary, session startup will fail with an error. Run `which claude-agent-acp`, `which codex-acp`, or `which gemini` to verify installation. diff --git a/documentation/docs/guides/cli-providers.md b/documentation/docs/guides/cli-providers.md index df80ce172c..b88c3c29f4 100644 --- a/documentation/docs/guides/cli-providers.md +++ b/documentation/docs/guides/cli-providers.md @@ -7,6 +7,10 @@ description: Use Claude Code, Codex, Cursor Agent, or Gemini CLI subscriptions i # CLI Providers +:::warning Deprecated — Use ACP Providers +The Claude Code (`claude-code`), Codex (`codex`), and Gemini CLI (`gemini-cli`) providers are deprecated. Use the [ACP providers](/docs/guides/acp-providers) (`claude-acp`, `codex-acp`, `gemini-acp`) instead, which support goose extensions via MCP and use the standardized Agent Client Protocol. CLI providers are kept for backward compatibility only. +::: + goose can make use of pass-through providers that integrate with existing CLI tools from Anthropic, OpenAI, Cursor, and Google. These providers allow you to use your existing Claude Code, Codex, Cursor Agent, and Google Gemini CLI subscriptions through goose's interface, adding session management, persistence, and workflow integration capabilities to these tools. :::warning Limitations From 28a52e36ff270b4782452019f342c57251201957 Mon Sep 17 00:00:00 2001 From: Elias Posen Date: Thu, 19 Mar 2026 04:55:46 -0400 Subject: [PATCH 3/6] Support CodeMode ToolDisclosure Variants (#7926) Signed-off-by: Elias Posen --- .gitignore | 1 + Cargo.lock | 113 ++++-- .../test_data/openai_builtin_execute.txt | 2 +- crates/goose-cli/src/session/output.rs | 6 +- .../platform_extensions/code_execution.rs | 337 ++++++++++-------- .../src/agents/platform_extensions/mod.rs | 8 +- crates/goose/src/agents/reply_parts.rs | 51 ++- ...nager__tests__all_platform_extensions.snap | 25 +- crates/goose/src/providers/local_inference.rs | 8 +- .../inference_emulated_tools.rs | 25 +- .../providers/local_inference/tool_parsing.rs | 4 +- .../docs/guides/managing-tools/code-mode.md | 8 +- scripts/test_providers_code_exec.sh | 5 +- .../src/components/ToolCallWithResponse.tsx | 8 +- 14 files changed, 367 insertions(+), 234 deletions(-) diff --git a/.gitignore b/.gitignore index 9254becb26..6b1ee2ce31 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ run_cli.sh tokenizer_files/ .DS_Store .idea +.vscode *.log tmp/ diff --git a/Cargo.lock b/Cargo.lock index 18405840ab..47fc6def4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", - "anstyle-parse", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -121,9 +136,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -134,6 +149,15 @@ dependencies = [ "utf8parse", ] +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + [[package]] name = "anstyle-query" version = "1.1.5" @@ -940,7 +964,7 @@ dependencies = [ "bincode", "bytesize", "clircle", - "console 0.16.2", + "console 0.16.3", "content_inspector", "encoding_rs", "flate2", @@ -964,7 +988,7 @@ dependencies = [ "syntect", "terminal-colorsaurus", "thiserror 2.0.18", - "toml", + "toml 0.9.12+spec-1.1.0", "unicode-segmentation", "unicode-width 0.2.2", "walkdir", @@ -1838,9 +1862,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -1848,11 +1872,11 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream", + "anstream 1.0.0", "anstyle", "clap_lex", "strsim", @@ -1860,18 +1884,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.66" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c757a3b7e39161a4e56f9365141ada2a6c915a8622c408ab6bb4b5d047371031" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -1881,15 +1905,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_mangen" -version = "0.2.31" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" +checksum = "7e30ffc187e2e3aeafcd1c6e2aa416e29739454c0ccaa419226d5ecd181f2d78" dependencies = [ "clap", "roff", @@ -1945,9 +1969,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -2029,9 +2053,9 @@ dependencies = [ [[package]] name = "config" -version = "0.15.19" +version = "0.15.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30fa8254caad766fc03cb0ccae691e14bf3bd72bfff27f72802ce729551b3d6" +checksum = "4fe5feec195269515c4722937cd7ffcfe7b4205d18d2e6577b7223ecb159ab00" dependencies = [ "async-trait", "convert_case 0.6.0", @@ -2042,7 +2066,7 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml", + "toml 1.0.6+spec-1.1.0", "winnow", "yaml-rust2", ] @@ -2062,13 +2086,12 @@ dependencies = [ [[package]] name = "console" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", "unicode-width 0.2.2", "windows-sys 0.61.2", ] @@ -4373,7 +4396,7 @@ dependencies = [ name = "goose-cli" version = "1.28.0" dependencies = [ - "anstream", + "anstream 0.6.21", "anyhow", "async-trait", "base64 0.22.1", @@ -4385,7 +4408,7 @@ dependencies = [ "clap_mangen", "cliclack", "comfy-table", - "console 0.16.2", + "console 0.16.3", "dotenvy", "etcetera 0.11.0", "futures", @@ -5224,7 +5247,7 @@ version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" dependencies = [ - "console 0.16.2", + "console 0.16.3", "portable-atomic", "unicode-width 0.2.2", "unit-prefix", @@ -7922,9 +7945,9 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +checksum = "dbf2048e0e979efb2ca7b91c4f1a8d77c91853e9b987c94c555668a8994915ad" [[package]] name = "ron" @@ -10349,12 +10372,25 @@ dependencies = [ "indexmap 2.13.0", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -10364,6 +10400,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_parser" version = "1.0.9+spec-1.1.0" @@ -10580,9 +10625,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", diff --git a/crates/goose-acp/tests/test_data/openai_builtin_execute.txt b/crates/goose-acp/tests/test_data/openai_builtin_execute.txt index bf6d35bd79..9b2edd9485 100644 --- a/crates/goose-acp/tests/test_data/openai_builtin_execute.txt +++ b/crates/goose-acp/tests/test_data/openai_builtin_execute.txt @@ -1,4 +1,4 @@ -data: {"id":"chatcmpl-D64NZp69RkEyXdUoDaCBj7fSYll8J","object":"chat.completion.chunk","created":1770339173,"model":"gpt-5-nano-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HCUq7OYIqj233H77wpqAtSGP","type":"function","function":{"name":"code_execution__execute","arguments":""}}],"refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"XbIx"} +data: {"id":"chatcmpl-D64NZp69RkEyXdUoDaCBj7fSYll8J","object":"chat.completion.chunk","created":1770339173,"model":"gpt-5-nano-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":null,"tool_calls":[{"index":0,"id":"call_HCUq7OYIqj233H77wpqAtSGP","type":"function","function":{"name":"code_execution__execute_typescript","arguments":""}}],"refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"XbIx"} data: {"id":"chatcmpl-D64NZp69RkEyXdUoDaCBj7fSYll8J","object":"chat.completion.chunk","created":1770339173,"model":"gpt-5-nano-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\""}}]},"finish_reason":null}],"usage":null,"obfuscation":"0WAOew1EJA"} diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index ea3f0678d4..950cd021fd 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -480,7 +480,7 @@ fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) { Ok(call) => match call.name.to_string().as_str() { name if is_shell_tool_name(name) => render_shell_request(call, debug), name if is_file_tool_name(name) => render_text_editor_request(call, debug), - "execute" | "execute_code" => render_execute_code_request(call, debug), + "execute_typescript" | "execute_code" => render_execute_code_request(call, debug), "delegate" => render_delegate_request(call, debug), "subagent" => render_delegate_request(call, debug), "todo__write" => render_todo_request(call, debug), @@ -822,7 +822,7 @@ pub fn render_subagent_tool_call( arguments: Option<&JsonObject>, debug: bool, ) { - if tool_name == "code_execution__execute_code" { + if tool_name == "code_execution__execute_typescript" { let tool_graph = arguments .and_then(|args| args.get("tool_graph")) .and_then(Value::as_array) @@ -851,7 +851,7 @@ fn render_subagent_tool_graph(subagent_id: &str, tool_graph: &[Value]) { " {} {} {} {} tool call{}", style("▸").dim(), style(format!("[subagent:{}]", short_id)).dim(), - style("execute_code").dim(), + style("execute_typescript").dim(), style(count).dim(), plural, ); diff --git a/crates/goose/src/agents/platform_extensions/code_execution.rs b/crates/goose/src/agents/platform_extensions/code_execution.rs index 22e0f43ae7..2601203567 100644 --- a/crates/goose/src/agents/platform_extensions/code_execution.rs +++ b/crates/goose/src/agents/platform_extensions/code_execution.rs @@ -4,11 +4,13 @@ use crate::agents::mcp_client::{Error, McpClientTrait}; use crate::agents::tool_execution::ToolCallContext; use anyhow::Result; use async_trait::async_trait; -use indoc::indoc; -use pctx_code_mode::config::ToolDisclosure; -use pctx_code_mode::model::{CallbackConfig, ExecuteInput, GetFunctionDetailsInput}; -use pctx_code_mode::registry::{CallbackFn, PctxRegistry}; -use pctx_code_mode::CodeMode; +use pctx_code_mode::{ + config::ToolDisclosure, + descriptions::{tools as tool_descriptions, workflow::get_workflow_description}, + model::{CallbackConfig, ExecuteBashInput, ExecuteInput, GetFunctionDetailsInput}, + registry::{CallbackFn, PctxRegistry}, + CodeMode, +}; use rmcp::model::{ CallToolRequestParams, CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult, RawContent, Role, ServerCapabilities, Tool as McpTool, ToolAnnotations, @@ -29,6 +31,7 @@ pub static EXTENSION_NAME: &str = "code_execution"; pub struct CodeExecutionClient { info: InitializeResult, context: PlatformExtensionContext, + disclosure: ToolDisclosure, state: RwLock>, } @@ -54,32 +57,18 @@ pub struct ExecuteWithToolGraph { } impl CodeExecutionClient { - pub fn new(context: PlatformExtensionContext) -> Result { + pub fn new(context: PlatformExtensionContext, disclosure: ToolDisclosure) -> Result { let info = InitializeResult::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info( Implementation::new(EXTENSION_NAME.to_string(), "1.0.0".to_string()) .with_title("Code Mode"), ) - .with_instructions(indoc! {r#" - BATCH MULTIPLE TOOL CALLS INTO ONE execute CALL. - - This extension exists to reduce round-trips. When a task requires multiple tool calls: - - WRONG: Multiple execute calls, each with one tool - - RIGHT: One execute call with a script that calls all needed tools - - IMPORTANT: All tool calls are ASYNC. Use await for each call. - - Workflow: - 1. Use the list_functions and get_function_details tools to discover tools and signatures - 2. Write ONE script that calls ALL tools needed for the task, no need to import anything, - all the namespaces returned by list_functions and get_function_details will be available - 3. Chain results: use output from one tool as input to the next - 4. Only return and console.log data you need, tools could have very large responses. - "#}.to_string()); + .with_instructions(get_workflow_description(disclosure)); Ok(Self { info, context, + disclosure, state: RwLock::new(None), }) } @@ -95,19 +84,20 @@ impl CodeExecutionClient { .get_prefixed_tools_excluding(session_id, EXTENSION_NAME) .await .ok()?; + let mut cfgs = vec![]; for tool in tools { - let full_name = tool.name.to_string(); - let (namespace, name) = if let Some((server, tool_name)) = full_name.split_once("__") { - (server.to_string(), tool_name.to_string()) + let (name, namespace) = if let Some((prefix, tool_name)) = tool.name.split_once("__") { + (tool_name.to_string(), Some(prefix.to_string())) } else if let Some(owner) = get_tool_owner(&tool) { - (owner, full_name) + (tool.name.to_string(), Some(owner)) } else { - continue; + (tool.name.to_string(), None) }; + cfgs.push(CallbackConfig { name, - namespace: Some(namespace), + namespace, description: tool.description.as_ref().map(|d| d.to_string()), input_schema: Some(json!(tool.input_schema)), output_schema: tool.output_schema.as_ref().map(|s| json!(s)), @@ -146,10 +136,11 @@ impl CodeExecutionClient { let state = CodeModeState::new(cfgs)?; let code_mode = state.code_mode.clone(); *guard = Some(state); + Ok(code_mode) } - /// Build a CallbackRegistry with all tool callbacks registered + /// Build a PctxRegistry with all tool callbacks registered fn build_callback_registry( &self, session_id: &str, @@ -164,7 +155,14 @@ impl CodeExecutionClient { let registry = PctxRegistry::default(); for cfg in code_mode.callbacks() { - let full_name = format!("{}__{}", cfg.namespace.as_deref().unwrap_or(""), &cfg.name); + let full_name = format!( + "{}{}", + cfg.namespace + .clone() + .map(|n| format!("{n}__")) + .unwrap_or_default(), + &cfg.name + ); let callback = create_tool_callback(session_id.to_string(), full_name, manager.clone()); registry .add_callback(&cfg.id(), callback) @@ -200,21 +198,19 @@ impl CodeExecutionClient { Ok(vec![Content::text(output.code)]) } - /// Handle the execute tool call - async fn handle_execute( + /// Handle the execute bash tool call + async fn handle_execute_bash( &self, session_id: &str, arguments: Option, ) -> Result, String> { - let args: ExecuteWithToolGraph = arguments + let input: ExecuteBashInput = arguments .map(|args| serde_json::from_value(Value::Object(args))) .transpose() .map_err(|e| format!("Failed to parse arguments: {e}"))? - .ok_or("Missing arguments for execute")?; - + .ok_or("Missing arguments for execute_bash")?; + let command = input.command; let code_mode = self.get_code_mode(session_id).await?; - let registry = self.build_callback_registry(session_id, &code_mode)?; - let code = args.input.code.clone(); // Deno runtime is not Send, so we need to run it in a blocking task // with its own tokio runtime @@ -226,13 +222,51 @@ impl CodeExecutionClient { rt.block_on(async move { code_mode - .execute_typescript(&code, ToolDisclosure::default(), Some(registry)) + .execute_bash(&command) .await - .map_err(|e| format!("Execution error: {e}")) + .map_err(|e| format!("Typescript execution error: {e}")) }) }) .await - .map_err(|e| format!("Execution task failed: {e}"))??; + .map_err(|e| format!("Typescript execution task failed: {e}"))??; + + Ok(vec![Content::text(output.markdown())]) + } + + /// Handle the execute typescript tool call + async fn handle_execute_typescript( + &self, + session_id: &str, + arguments: Option, + ) -> Result, String> { + let args: ExecuteWithToolGraph = arguments + .map(|args| serde_json::from_value(Value::Object(args))) + .transpose() + .map_err(|e| format!("Failed to parse arguments: {e}"))? + .ok_or("Missing arguments for execute_typescript")?; + + let code_mode = self.get_code_mode(session_id).await?; + let registry = self.build_callback_registry(session_id, &code_mode)?; + let code = args.input.code.clone(); + let disclosure = self.disclosure; + + // Deno runtime is not Send, so we need to run it in a blocking task + // with its own tokio runtime + let output = tokio::task::spawn_blocking(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| format!("Failed to create runtime: {e}"))?; + + rt.block_on(async move { + code_mode + .execute_typescript(&code, disclosure, Some(registry)) + .await + .map_err(|e| format!("Typescript execution error: {e}")) + }) + }) + .await + .map_err(|e| format!("Typescript execution task failed: {e}"))??; Ok(vec![Content::text(output.markdown())]) } @@ -316,97 +350,79 @@ impl McpClientTrait for CodeExecutionClient { })) .expect("valid schema"); - Ok(ListToolsResult { - tools: vec![ - McpTool::new( - "list_functions".to_string(), - indoc! {r#" - List all available functions across all namespaces. - - This will not return function input and output types. - After determining which functions are needed use - get_function_details to get input and output type - information about specific functions. - "#} - .to_string(), - empty_schema, - ) - .annotate(ToolAnnotations::from_raw( - Some("List functions".to_string()), - Some(true), - Some(false), - Some(true), - Some(false), - )), - McpTool::new( - "get_function_details".to_string(), - indoc! {r#" - Get detailed type information for specific functions. - - Provide a list of function identifiers in the format "Namespace.functionName" - (e.g., "Developer.shell", "Github.createIssue"). - - Returns full TypeScript interface definitions with parameter types, - return types, and descriptions for the requested functions. - "#} - .to_string(), - schema::(), - ) - .annotate(ToolAnnotations::from_raw( - Some("Get function details".to_string()), - Some(true), - Some(false), - Some(true), - Some(false), - )), - McpTool::new( - "execute".to_string(), - indoc! {r#" - Execute TypeScript code that calls available functions. - - SYNTAX - TypeScript with async run() function: - ```typescript - async function run() { - // Access functions via Namespace.functionName({ params }) — always camelCase - const files = await Developer.shell({ command: "ls -la" }); - const readme = await Developer.shell({ command: "cat ./README.md" }); - return { files, readme }; - } - ``` - - TOOL_GRAPH: Always provide tool_graph to describe the execution flow for the UI. - Each node has: tool (Namespace.functionName), description (what it does), depends_on (indices of dependencies). - Example for chained operations: - [ - {"tool": "Developer.shell", "description": "list files", "depends_on": []}, - {"tool": "Developer.shell", "description": "read README.md", "depends_on": []}, - {"tool": "Developer.write", "description": "write output.txt", "depends_on": [0, 1]} - ] - - KEY RULES: - - Code MUST define an async function named `run()` - - All function calls are async - use `await` - - Function names are always camelCase (e.g., Developer.shell, Github.listIssues, Github.createIssue) - - Return value from `run()` is the result, all `console.log()` output will be returned as well. - - Only functions from `list_functions()` and `console` methods are available — no `fetch()`, `fs`, or other Node/Deno APIs - - Variables don't persist between `execute()` calls - return or log anything you need later - - Code runs in an isolated sandbox with restricted network access - - HANDLING RETURN VALUES: - - If a function returns `any`, do NOT assume its shape - log it first: `console.log(JSON.stringify(result))` - - Many functions return wrapper objects, not raw arrays - check the response structure before calling .filter(), .map(), etc. - - Always inspect unfamiliar return values with console.log() before processing them - - TOKEN USAGE WARNING: This tool could return LARGE responses if your code returns big objects. - To minimize tokens: - - Filter/map/reduce data IN YOUR CODE before returning - - Only return specific fields you need (e.g., return {id: result.id, count: items.length}) - - Use console.log() for intermediate results instead of returning everything - - Avoid returning full API responses - extract just what you need - - BEFORE CALLING: Use list_functions or get_function_details to check available functions and their parameters. - "#} - .to_string(), + let tools = match self.disclosure { + ToolDisclosure::Catalog => { + vec![ + McpTool::new( + "list_functions".to_string(), + tool_descriptions::LIST_FUNCTIONS.to_string(), + empty_schema, + ) + .annotate(ToolAnnotations::from_raw( + Some("List functions".to_string()), + Some(true), + Some(false), + Some(true), + Some(false), + )), + McpTool::new( + "get_function_details".to_string(), + tool_descriptions::GET_FUNCTION_DETAILS.to_string(), + schema::(), + ) + .annotate(ToolAnnotations::from_raw( + Some("Get function details".to_string()), + Some(true), + Some(false), + Some(true), + Some(false), + )), + McpTool::new( + "execute_typescript".to_string(), + tool_descriptions::EXECUTE_TYPESCRIPT_CATALOG.to_string(), + schema::(), + ) + .annotate(ToolAnnotations::from_raw( + Some("Execute TypeScript".to_string()), + Some(false), + Some(true), + Some(false), + Some(true), + )), + ] + } + ToolDisclosure::Filesystem => { + vec![ + McpTool::new( + "execute_bash".to_string(), + tool_descriptions::EXECUTE_BASH.to_string(), + schema::(), + ) + .annotate(ToolAnnotations::from_raw( + Some("Get function details".to_string()), + Some(true), + Some(false), + Some(true), + Some(false), + )), + McpTool::new( + "execute_typescript".to_string(), + tool_descriptions::EXECUTE_TYPESCRIPT_FILESYSTEM.to_string(), + schema::(), + ) + .annotate(ToolAnnotations::from_raw( + Some("Execute TypeScript".to_string()), + Some(false), + Some(true), + Some(false), + Some(true), + )), + ] + } + ToolDisclosure::Sidecar => { + vec![McpTool::new( + "execute_typescript".to_string(), + tool_descriptions::EXECUTE_TYPESCRIPT_SIDECAR.to_string(), schema::(), ) .annotate(ToolAnnotations::from_raw( @@ -415,10 +431,14 @@ impl McpClientTrait for CodeExecutionClient { Some(true), Some(false), Some(true), - )), - ], - next_cursor: None, + ))] + } + }; + + Ok(ListToolsResult { meta: None, + next_cursor: None, + tools, }) } @@ -436,7 +456,8 @@ impl McpClientTrait for CodeExecutionClient { self.handle_get_function_details(session_id, arguments) .await } - "execute" => self.handle_execute(session_id, arguments).await, + "execute_bash" => self.handle_execute_bash(session_id, arguments).await, + "execute_typescript" => self.handle_execute_typescript(session_id, arguments).await, _ => Err(format!("Unknown tool: {name}")), }; @@ -454,28 +475,48 @@ impl McpClientTrait for CodeExecutionClient { async fn get_moim(&self, session_id: &str) -> Option { let code_mode = self.get_code_mode(session_id).await.ok()?; - let available: Vec<_> = code_mode - .list_functions() - .functions - .iter() - .map(|f| format!("{}.{}", &f.namespace, &f.name)) - .collect(); + + let disclosure_style_moim = match self.disclosure { + ToolDisclosure::Catalog => { + let available_fns: Vec<_> = code_mode + .list_functions() + .functions + .iter() + .map(|f| format!("{}.{}", &f.namespace, &f.name)) + .collect(); + format!("Available functions: {} + + Use the list_functions & get_function_details tools to see tool signatures and input/output types before calling execute_typescript.", available_fns.join(", ")) + } + ToolDisclosure::Filesystem => { + let available_filepaths: Vec<_> = code_mode + .virtual_fs().keys().map(String::from).collect(); + format!("Use execute_bash to search and read the tool signatures and input/output types before calling execute_typescript. The available files are: {}", available_filepaths.join(", ")) + }, + ToolDisclosure::Sidecar => "Prioritize calling tools with the execute_typescript tool, especially when multiple tools can be called in one script.".into(), + }; Some(format!( indoc::indoc! {r#" - ALWAYS batch multiple tool operations into ONE execute call. - - WRONG: Separate execute calls for read file, then write file - - RIGHT: One execute with an async run() function that reads AND writes + ALWAYS batch multiple tool operations into ONE execute_typescript call. + - WRONG: Separate execute_typescript calls for read file, then write file + - RIGHT: One execute_typescript with an async run() function that reads AND writes AND logs/returns as little information as needed for the next step. - Available namespaces: {} - - Use the list_functions & get_function_details tools to see tool signatures and input/output types before calling unfamiliar tools. + {} "#}, - available.join(", ") + disclosure_style_moim )) } } +pub fn get_tool_disclosure() -> ToolDisclosure { + let config = crate::config::Config::global(); + let tool_disclosure_str: String = config + .get_param("CODE_MODE_TOOL_DISCLOSURE") + .unwrap_or_else(|_| "catalog".to_string()); + serde_json::from_value(serde_json::json!(tool_disclosure_str)).unwrap_or_default() +} + struct CodeModeState { code_mode: CodeMode, hash: u64, diff --git a/crates/goose/src/agents/platform_extensions/mod.rs b/crates/goose/src/agents/platform_extensions/mod.rs index cd577d71e8..04eecb2cf2 100644 --- a/crates/goose/src/agents/platform_extensions/mod.rs +++ b/crates/goose/src/agents/platform_extensions/mod.rs @@ -128,7 +128,13 @@ pub static PLATFORM_EXTENSIONS: Lazy default_enabled: false, unprefixed_tools: true, client_factory: |ctx| { - Box::new(code_execution::CodeExecutionClient::new(ctx).unwrap()) + Box::new( + code_execution::CodeExecutionClient::new( + ctx, + code_execution::get_tool_disclosure(), + ) + .unwrap(), + ) }, }, ); diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index e40f649ea7..7046a729f4 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -155,13 +155,50 @@ impl Agent { #[cfg(not(feature = "code-mode"))] let code_execution_active = false; if code_execution_active { - tools.retain(|tool| { - if let Some(owner) = crate::agents::extension_manager::get_tool_owner(tool) { - crate::agents::extension_manager::is_first_class_extension(&owner) - } else { - false - } - }); + let disclosure_style = + crate::agents::platform_extensions::code_execution::get_tool_disclosure(); + + tools = tools + .into_iter() + .filter_map(|mut t| match disclosure_style { + pctx_code_mode::config::ToolDisclosure::Catalog + | pctx_code_mode::config::ToolDisclosure::Filesystem => { + // in catalog & filesystem styles, progressive search is handled + // by pctx, so we want to omit all non-first-class extensions + // from the standard tool list + if crate::agents::extension_manager::get_tool_owner(&t).is_some_and(|o| { + crate::agents::extension_manager::is_first_class_extension(&o) + }) { + Some(t) + } else { + None + } + } + pctx_code_mode::config::ToolDisclosure::Sidecar => { + // in sidecar style there is no progressive search, just a way to chain tools + // together with typescript + // add output schema to description since many model providers drop the + // output schema when presenting tools to the model + let output_schema = t + .output_schema + .as_ref() + .map(|s| serde_json::json!(s).to_string()) + .unwrap_or("unknown".to_string()); + let description_extension = format!( + "The successful return schema of this tool is:\n{output_schema}" + ); + + t.description = Some( + t.description + .map(|t| format!("{t}\n{description_extension}")) + .unwrap_or(description_extension) + .into(), + ); + + Some(t) + } + }) + .collect(); } // Stable tool ordering is important for multi session prompt caching. diff --git a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap index 1b00f5374e..124cc17058 100644 --- a/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap +++ b/crates/goose/src/agents/snapshots/goose__agents__prompt_manager__tests__all_platform_extensions.snap @@ -64,21 +64,16 @@ Two modes: ## code_execution ### Instructions -BATCH MULTIPLE TOOL CALLS INTO ONE execute CALL. - -This extension exists to reduce round-trips. When a task requires multiple tool calls: -- WRONG: Multiple execute calls, each with one tool -- RIGHT: One execute call with a script that calls all needed tools - -IMPORTANT: All tool calls are ASYNC. Use await for each call. - -Workflow: - 1. Use the list_functions and get_function_details tools to discover tools and signatures - 2. Write ONE script that calls ALL tools needed for the task, no need to import anything, - all the namespaces returned by list_functions and get_function_details will be available - 3. Chain results: use output from one tool as input to the next - 4. Only return and console.log data you need, tools could have very large responses. - +General: + - BATCH MULTIPLE TOOL CALLS INTO ONE `execute_typescript` CALL. + - These tools exists to reduce round-trips. When a task requires multiple tool calls: + - WRONG: Multiple `execute_typescript` calls, each with one tool + - RIGHT: One `execute_typescript` call with a script that calls all needed tools + - Only `return` and `console.log` data you need, tools could have very large responses. + - IMPORTANT: All tool calls are ASYNC. Use await for each call. +WORKFLOW: + 1. Use the `list_functions` and `get_function_details` tools to discover tools signatures and input/output types. + 2. Write ONE script that calls ALL tools needed for the task and execute that script with `execute_typescript`, no need to import anything, all the namespaces returned by `list_functions` and `get_function_details` will be available globally. ## developer ### Instructions diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index 2020699a85..04bac00f1f 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -39,7 +39,7 @@ use tokio::sync::Mutex; use uuid::Uuid; const SHELL_TOOL: &str = "developer__shell"; -const CODE_EXECUTION_TOOL: &str = "code_execution__execute"; +const CODE_EXECUTION_TOOL: &str = "code_execution__execute_typescript"; type ModelSlot = Arc>>; @@ -210,10 +210,10 @@ fn build_openai_messages_json(system: &str, messages: &[Message]) -> String { /// the model: /// /// - `ToolRequest` with a `"command"` argument → `$ command` -/// - `ToolRequest` with a `"code"` argument → `` ```execute\n…\n``` `` +/// - `ToolRequest` with a `"code"` argument → `` ```execute_typescript\n…\n``` `` /// - `ToolResponse` → `Command output:\n…` /// -/// Only `developer__shell` and `code_execution__execute` style tool calls are +/// Only `developer__shell` and `code_execution__execute_typescript` style tool calls are /// recognized (by argument shape, not tool name). Tool calls from other extensions /// (e.g. custom MCP tools made by a native-tool-calling model earlier in the /// conversation) are silently dropped, since the emulator path has no syntax to @@ -241,7 +241,7 @@ fn extract_text_content(msg: &Message) -> String { .and_then(|a| a.get("code")) .and_then(|v| v.as_str()) { - parts.push(format!("```execute\n{}\n```", code)); + parts.push(format!("```execute_typescript\n{}\n```", code)); } } } diff --git a/crates/goose/src/providers/local_inference/inference_emulated_tools.rs b/crates/goose/src/providers/local_inference/inference_emulated_tools.rs index 902bdec273..9e04852a4a 100644 --- a/crates/goose/src/providers/local_inference/inference_emulated_tools.rs +++ b/crates/goose/src/providers/local_inference/inference_emulated_tools.rs @@ -34,7 +34,7 @@ use super::inference_engine::{ }; use super::{finalize_usage, StreamSender, CODE_EXECUTION_TOOL, SHELL_TOOL}; -const HOLD_BACK_CODE_MODE: usize = " ```execute\n".len(); +const HOLD_BACK_CODE_MODE: usize = " ```execute_typescript\n".len(); const HOLD_BACK_SHELL_ONLY: usize = "\n$".len(); pub(super) fn load_tiny_model_prompt() -> String { @@ -79,7 +79,7 @@ pub(super) fn build_emulator_tool_description(tools: &[Tool], code_mode_enabled: The code runs immediately — do not explain it, just run it.\n\n", ); tool_desc.push_str("Example — counting files in /tmp:\n\n"); - tool_desc.push_str("```execute\nasync function run() {\n"); + tool_desc.push_str("```execute_typescript\nasync function run() {\n"); tool_desc.push_str( " const result = await Developer.shell({ command: \"ls -1 /tmp | wc -l\" });\n", ); @@ -206,7 +206,9 @@ impl StreamingEmulatorParser { ParserState::Normal => { // Check for ```execute block (code mode) if self.code_mode_enabled { - if let Some((before, after)) = self.buffer.split_once("```execute\n") { + if let Some((before, after)) = + self.buffer.split_once("```execute_typescript\n") + { if !before.trim().is_empty() { results.push(EmulatorAction::Text(before.to_string())); } @@ -215,8 +217,8 @@ impl StreamingEmulatorParser { continue; } // Also handle without newline after tag (accumulating) - if self.buffer.ends_with("```execute") { - let before = self.buffer.trim_end_matches("```execute"); + if self.buffer.ends_with("```execute_typescript") { + let before = self.buffer.trim_end_matches("```execute_typescript"); if !before.trim().is_empty() { results.push(EmulatorAction::Text(before.to_string())); } @@ -561,7 +563,7 @@ mod tests { #[test] fn execute_block() { - let input = "Here's the code:\n```execute\nconsole.log('hi');\n```\n"; + let input = "Here's the code:\n```execute_typescript\nconsole.log('hi');\n```\n"; let actions = parse_all(input, true); assert!(actions.len() >= 2); assert_text(&actions[0], "Here's the code:"); @@ -570,7 +572,7 @@ mod tests { #[test] fn execute_block_not_detected_without_code_mode() { - let input = "```execute\nconsole.log('hi');\n```\n"; + let input = "```execute_typescript\nconsole.log('hi');\n```\n"; let actions = parse_all(input, false); // Should be treated as plain text for action in &actions { @@ -592,7 +594,10 @@ mod tests { #[test] fn execute_fence_split_across_chunks() { - let actions = parse_chunks(&["Here:\n```ex", "ecute\nlet x = 1;\n", "```\n"], true); + let actions = parse_chunks( + &["Here:\n```ex", "ecute_typescript\nlet x = 1;\n", "```\n"], + true, + ); let executes: Vec<_> = actions .iter() .filter(|a| matches!(a, EmulatorAction::ExecuteCode(_))) @@ -655,7 +660,7 @@ mod tests { #[test] fn execute_block_with_multiline_code() { - let input = "```execute\nasync function run() {\n const r = await Developer.shell({ command: \"ls\" });\n return r;\n}\n```\n"; + let input = "```execute_typescript\nasync function run() {\n const r = await Developer.shell({ command: \"ls\" });\n return r;\n}\n```\n"; let actions = parse_all(input, true); let executes: Vec<_> = actions .iter() @@ -674,7 +679,7 @@ mod tests { #[test] fn unclosed_execute_block_flushed() { // Model stops generating mid-block - let input = "```execute\nlet x = 1;"; + let input = "```execute_typescript\nlet x = 1;"; let actions = parse_all(input, true); let executes: Vec<_> = actions .iter() diff --git a/crates/goose/src/providers/local_inference/tool_parsing.rs b/crates/goose/src/providers/local_inference/tool_parsing.rs index b03a679262..d2f57688b2 100644 --- a/crates/goose/src/providers/local_inference/tool_parsing.rs +++ b/crates/goose/src/providers/local_inference/tool_parsing.rs @@ -436,13 +436,13 @@ mod tests { #[test] fn test_parse_glm_style_tool_call_multiple_args() { - let text = "Let me check.\nexecutecodeasync function run() { return 1; }tool_graph[{\"tool\": \"shell\"}]"; + let text = "Let me check.\nexecute_typescriptcodeasync function run() { return 1; }tool_graph[{\"tool\": \"shell\"}]"; let result = split_content_and_xml_tool_calls(text); assert!(result.is_some()); let (content, calls) = result.unwrap(); assert_eq!(content, "Let me check."); assert_eq!(calls.len(), 1); - assert_eq!(calls[0].0, "execute"); + assert_eq!(calls[0].0, "execute_typescript"); assert_eq!( calls[0].1.get("code").unwrap(), "async function run() { return 1; }" diff --git a/documentation/docs/guides/managing-tools/code-mode.md b/documentation/docs/guides/managing-tools/code-mode.md index 49883b3ff2..8d8a373aac 100644 --- a/documentation/docs/guides/managing-tools/code-mode.md +++ b/documentation/docs/guides/managing-tools/code-mode.md @@ -15,13 +15,15 @@ This functionality requires the built-in [Code Mode extension](/docs/mcp/code-mo ::: Code Mode controls how tools are discovered and called: + - Tools from enabled extensions are discovered on-demand and loaded into context as needed - Multiple tool calls are batched in one execution - Intermediate results are chained (output from one tool as input to the next) ## How Code Mode Works -The [Code Mode extension](/docs/mcp/code-mode-mcp) is an MCP server that uses the MCP protocol to expose three foundational meta-tools. When Code Mode is enabled, goose switches to Code Mode. For every request, the LLM writes JavaScript code that goose executes using [pctx (Port of Context)](https://github.com/AdrianCole/pctx), a custom Deno-based runtime, to: +The [Code Mode extension](/docs/mcp/code-mode-mcp) is an MCP server that uses the MCP protocol to expose three foundational meta-tools. When Code Mode is enabled, goose switches to Code Mode. For every request, the LLM writes JavaScript code that goose executes using [pctx (Port of Context)](https://portofcontext.com/) ([GitHub](https://github.com/portofcontext/pctx)), a custom Deno-based runtime, to: + - Discover available tools from your enabled extensions (if needed) - Learn how to work with the tools it needs for the current task - Call those tools programmatically to complete the task @@ -32,7 +34,7 @@ Traditional MCP tool calling and Code Mode are two different approaches to the s | Aspect | Traditional | Code Mode | |--------|------------------|-----------| -| **Tool Discovery** | All tools from enabled extensions, for example:
• `developer.shell`
• `developer.text_editor`
• `github.list_issues`
• `github.get_pull_request`
• `slack.send_message`
• ... *potentially many more* | Code Mode extension's meta-tools:
• `list_functions`
• `get_function_details`
• `execute`

The LLM uses these tools to discover tools from other enabled extensions as needed | +| **Tool Discovery** | All tools from enabled extensions, for example:
• `developer.shell`
• `developer.text_editor`
• `github.list_issues`
• `github.get_pull_request`
• `slack.send_message`
• ... *potentially many more* | Code Mode extension's meta-tools:
• `list_functions`
• `get_function_details`
• `execute_typescript`

The LLM uses these tools to discover tools from other enabled extensions as needed | | **Tool Calling** | • Sequential tool calls
• Each result sent to the LLM before the next call | • May require tool discovery calls
• Multiple tool calls batched in one execution
• Intermediate results are chained and processed locally | | **Context Window** | Every LLM call includes all tool definitions from enabled extensions | Every LLM call includes the 3 meta-tool definitions, plus any tool definitions previously discovered in the session | | **Best For** | • 1-3 enabled extensions
• Simple tasks using 1-2 tools | • 5+ extensions
• Well-defined multi-step workflows | @@ -68,4 +70,4 @@ import notMcpReplacement from '@site/blog/2025-12-21-code-mode-doesnt-replace-mc duration: '8 min read' } ]} -/> +/> \ No newline at end of file diff --git a/scripts/test_providers_code_exec.sh b/scripts/test_providers_code_exec.sh index 19243d6622..c9d720d202 100755 --- a/scripts/test_providers_code_exec.sh +++ b/scripts/test_providers_code_exec.sh @@ -27,10 +27,11 @@ run_test() { cd "$testdir" && "$GOOSE_BIN" run --text "$prompt" --with-builtin "$BUILTINS" 2>&1 ) > "$output_file" 2>&1 - # Matches: "execute | code_execution", "get_function_details | code_execution", + # Matches: "execute_typescript | code_execution", "get_function_details | code_execution", # "tool call | execute", "tool calls | execute" (old format) # "▸ execute N tool call" (new format with tool_graph) - if grep -qE "(execute \| code_execution)|(get_function_details \| code_execution)|(tool calls? \| execute)|(▸.*execute.*tool call)" "$output_file"; then + # "▸ execute_typescript" (plain tool name in output) + if grep -qE "(execute_typescript \| code_execution)|(get_function_details \| code_execution)|(tool calls? \| execute)|(▸.*execute.*tool call)|(▸ execute_typescript)" "$output_file"; then echo "success|code_execution tool called" > "$result_file" else echo "failure|no code_execution tool calls found" > "$result_file" diff --git a/ui/desktop/src/components/ToolCallWithResponse.tsx b/ui/desktop/src/components/ToolCallWithResponse.tsx index cbca5a4b7d..f3c3df5d3c 100644 --- a/ui/desktop/src/components/ToolCallWithResponse.tsx +++ b/ui/desktop/src/components/ToolCallWithResponse.tsx @@ -354,9 +354,9 @@ const formatSubagentToolCall = (data: SubagentToolRequestData): string => { const extensionName = parts.slice(1).reverse().join('__') || ''; const toolGraph = toolCall.arguments?.tool_graph; - if (toolName === 'execute_code' && toolGraph && toolGraph.length > 0) { + if (toolName === 'execute_typescript' && toolGraph && toolGraph.length > 0) { const plural = toolGraph.length === 1 ? '' : 's'; - const header = `[subagent:${shortId}] ${toolGraph.length} tool call${plural} | execute_code`; + const header = `[subagent:${shortId}] ${toolGraph.length} tool call${plural} | execute_typescript`; const lines = toolGraph.map((node, idx) => { const deps = node.depends_on && node.depends_on.length > 0 @@ -638,7 +638,7 @@ function ToolCallView({ case 'computer_control': return `poking around...`; - case 'execute': { + case 'execute_typescript': { const toolGraph = args.tool_graph as unknown as ToolGraphNode[] | undefined; if (toolGraph && Array.isArray(toolGraph) && toolGraph.length > 0) { if (toolGraph.length === 1) { @@ -736,7 +736,7 @@ function ToolCallView({ const toolGraph = toolCall.arguments?.tool_graph as unknown as ToolGraphNode[] | undefined; if ( - toolCall.name === 'code_execution__execute' && + toolCall.name === 'code_execution__execute_typescript' && (typeof code === 'string' || Array.isArray(toolGraph)) ) { return ( From c97fbb9f3cef8593586633f91ea221250c5c38c9 Mon Sep 17 00:00:00 2001 From: Rabi Mishra Date: Thu, 19 Mar 2026 15:22:40 +0530 Subject: [PATCH 4/6] fix: emit user-facing message on compaction failure (#7975) Signed-off-by: Rabi Mishra --- crates/goose/src/agents/agent.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 3f32950166..36c179fed3 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -1533,6 +1533,11 @@ impl Agent { Err(e) => { crate::posthog::emit_error("compaction_failed", &e.to_string()); error!("Compaction failed: {}", e); + yield AgentEvent::Message( + Message::assistant().with_text( + format!("Ran into this error trying to compact: {e}.\n\nPlease try again or create a new session") + ) + ); break; } } From a68c6788eb5b3adf98e725890696f1efdbe5f296 Mon Sep 17 00:00:00 2001 From: fre$h Date: Thu, 19 Mar 2026 17:54:36 +0000 Subject: [PATCH 5/6] feat(cli): add Sigstore/SLSA provenance verification to Rust self-update (#7818) Signed-off-by: fre$h Signed-off-by: fre --- .github/workflows/ci.yml | 2 +- Cargo.lock | 1360 ++++++++++++++++++-- crates/goose-cli/Cargo.toml | 2 + crates/goose-cli/src/commands/update.rs | 319 ++++- crates/goose/src/recipe/template_recipe.rs | 16 +- 5 files changed, 1576 insertions(+), 123 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a4be1ed8d..1cdac07e97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: # play nicely with hermit-managed rust hermit uninstall rustup export CARGO_INCREMENTAL=0 - cargo clippy --all-targets -- -D warnings + cargo clippy --workspace --all-targets --exclude v8 -- -D warnings - name: Check for banned TLS crates run: ./scripts/check-no-native-tls.sh diff --git a/Cargo.lock b/Cargo.lock index 47fc6def4b..d845f88965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -42,6 +52,17 @@ dependencies = [ "strum", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -235,6 +256,45 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -274,6 +334,17 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -370,12 +441,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed8cd42adddefbdb8507fb7443fa9b666631078616b78f70ed22117b5c27d90" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "regex", +] + [[package]] name = "aws-lc-rs" version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ + "aws-lc-fips-sys", "aws-lc-sys", "untrusted 0.7.1", "zeroize", @@ -387,6 +473,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ + "bindgen", "cc", "cmake", "dunce", @@ -913,6 +1000,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base32" version = "0.5.1" @@ -1440,9 +1533,9 @@ checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426" [[package]] name = "bpaf" -version = "0.9.23" +version = "0.9.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a59e5c2bac9ccb280150ef4b58e07e4169d8c5c6ce0f70170e3c0b7c7b6124" +checksum = "b2435ff2f08be8436bdcd06a3de2bd7696fd10e45eb630ecfc09af7fbfa3e69a" dependencies = [ "bpaf_derive", ] @@ -1766,9 +1859,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1847,6 +1940,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -2053,9 +2147,9 @@ dependencies = [ [[package]] name = "config" -version = "0.15.21" +version = "0.15.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe5feec195269515c4722937cd7ffcfe7b4205d18d2e6577b7223ecb159ab00" +checksum = "8e68cfe19cd7d23ffde002c24ffa5cda73931913ef394d5eaaa32037dc940c0c" dependencies = [ "async-trait", "convert_case 0.6.0", @@ -2066,8 +2160,8 @@ dependencies = [ "serde-untagged", "serde_core", "serde_json", - "toml 1.0.6+spec-1.1.0", - "winnow", + "toml 1.0.7+spec-1.1.0", + "winnow 1.0.0", "yaml-rust2", ] @@ -2122,6 +2216,26 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -2371,6 +2485,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -2381,6 +2507,21 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + [[package]] name = "ctor" version = "0.2.9" @@ -2412,6 +2553,33 @@ dependencies = [ "libloading 0.9.0", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "darling" version = "0.20.11" @@ -2422,16 +2590,6 @@ dependencies = [ "darling_macro 0.20.11", ] -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - [[package]] name = "darling" version = "0.23.0" @@ -2456,20 +2614,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - [[package]] name = "darling_core" version = "0.23.0" @@ -2494,17 +2638,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - [[package]] name = "darling_macro" version = "0.23.0" @@ -2614,6 +2747,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "decoded-char" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5440d1dc8ea7cae44cda3c64568db29bfa2434aba51ae66a50c00488841a65a3" + [[package]] name = "deno_ast" version = "0.52.0" @@ -2971,10 +3110,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", + "der_derive", + "flagset", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "deranged" version = "0.5.8" @@ -2982,6 +3148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -3278,6 +3445,20 @@ dependencies = [ "cipher", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ecow" version = "0.2.6" @@ -3287,6 +3468,31 @@ dependencies = [ "serde", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -3296,6 +3502,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email_address" version = "0.2.9" @@ -3364,6 +3591,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcb71a32ab9582e2756554e84b24aee90d7187034049c35921ec3296b70c13ad" +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equivalent" version = "1.0.2" @@ -3535,6 +3768,22 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.27" @@ -3567,6 +3816,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -4139,6 +4394,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -4191,6 +4447,18 @@ dependencies = [ "wasip3", ] +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "gif" version = "0.13.3" @@ -4241,7 +4509,7 @@ name = "goose" version = "1.28.0" dependencies = [ "agent-client-protocol-schema", - "ahash", + "ahash 0.8.12", "anyhow", "async-stream", "async-trait", @@ -4425,7 +4693,9 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha2", "shlex", + "sigstore-verification", "strum", "tar", "tempfile", @@ -4537,6 +4807,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gzip-header" version = "1.0.0" @@ -4620,6 +4901,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -4627,7 +4911,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "allocator-api2", ] @@ -4770,6 +5054,15 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "memchr", +] + [[package]] name = "http-body" version = "0.4.6" @@ -5338,6 +5631,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -5427,9 +5729,9 @@ dependencies = [ [[package]] name = "jni" -version = "0.22.3" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295dc9997acda1562fdf8d299f56063c936443b60f078e63a5d8d3c34ef2642b" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ "cfg-if", "combine", @@ -5444,9 +5746,9 @@ dependencies = [ [[package]] name = "jni-macros" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c3d1da60c95c98847b26b9d45f4360fee718b31de746df016d9cd6de916a7ef" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ "proc-macro2", "quote", @@ -5509,6 +5811,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-number" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479dfd2ad8e4b4ae076b031f72ef2f3791f65e2a0f51e5f3408dbf716c4c2f82" +dependencies = [ + "lexical", + "ryu-js 0.2.2", + "serde", + "smallvec", +] + +[[package]] +name = "json-syntax" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044a68aba3f96d712f492b72be25e10f96201eaaca3207a7d6e68d6d5105fda9" +dependencies = [ + "decoded-char", + "hashbrown 0.12.3", + "indexmap 1.9.3", + "json-number", + "locspan", + "locspan-derive", + "ryu-js 0.2.2", + "serde", + "smallstr", + "smallvec", + "utf8-decode", +] + [[package]] name = "json5" version = "0.4.1" @@ -5545,7 +5878,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" dependencies = [ - "ahash", + "ahash 0.8.12", "base64 0.22.1", "bytecount", "email_address", @@ -5583,6 +5916,21 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "keyring" version = "3.6.3" @@ -5644,6 +5992,72 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +[[package]] +name = "lexical" +version = "7.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc8a009b2ff1f419ccc62706f04fe0ca6e67b37460513964a3dfdb919bb37d6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + [[package]] name = "libc" version = "0.2.183" @@ -5745,9 +6159,9 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "llama-cpp-2" -version = "0.1.138" +version = "0.1.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2947ab625c59d1fdf42e61f538c3fa66f43de2f78316971920873f359483d1d8" +checksum = "d1b92f824729095247507a51f5ef7100adc70bbb4f86f1c4fdccbe4afd356bb9" dependencies = [ "encoding_rs", "enumflags2", @@ -5759,9 +6173,9 @@ dependencies = [ [[package]] name = "llama-cpp-sys-2" -version = "0.1.138" +version = "0.1.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a529006bf16af70c7485ba957820dc2bc9467d75697e97970c81d2da73c76f" +checksum = "e762baca42996077c9b3e5af393d585ca5299977faacf8ac4391f2dbd013a5d5" dependencies = [ "bindgen", "cc", @@ -5780,6 +6194,24 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "locspan" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33890449fcfac88e94352092944bf321f55e5deb4e289a6f51c87c55731200a0" + +[[package]] +name = "locspan-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88991223b049a3d29ca1f60c05639581336a0f3ee4bf8a659dddecc11c4961a" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "log" version = "0.4.29" @@ -5951,12 +6383,11 @@ dependencies = [ [[package]] name = "minijinja" -version = "2.17.1" +version = "2.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea5ea1e90055f200af6b8e52a4a34e05e77e7fee953a9fb40c631efdc43cab1" +checksum = "328251e58ad8e415be6198888fc207502727dc77945806421ab34f35bf012e7d" dependencies = [ "memo-map", - "self_cell", "serde", ] @@ -6051,6 +6482,12 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "nanoid" version = "0.4.0" @@ -6407,6 +6844,69 @@ dependencies = [ "memchr", ] +[[package]] +name = "oci-client" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b74df13319e08bc386d333d3dc289c774c88cc543cae31f5347db07b5ec2172" +dependencies = [ + "bytes", + "chrono", + "futures-util", + "http 1.4.0", + "http-auth", + "jwt", + "lazy_static", + "oci-spec", + "olpc-cjson", + "regex", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tracing", + "unicase", +] + +[[package]] +name = "oci-spec" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3da52b83ce3258fbf29f66ac784b279453c2ac3c22c5805371b921ede0d308" +dependencies = [ + "const_format", + "derive_builder", + "getset", + "regex", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror 2.0.18", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "olpc-cjson" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -6441,6 +6941,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.3.3" @@ -6452,6 +6958,37 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openidconnect" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c6709ba2ea764bbed26bce1adf3c10517113ddea6f2d4196e4851757ef2b2" +dependencies = [ + "base64 0.21.7", + "chrono", + "dyn-clone", + "ed25519-dalek", + "hmac", + "http 1.4.0", + "itertools 0.10.5", + "log", + "oauth2", + "p256", + "p384", + "rand 0.8.5", + "rsa", + "serde", + "serde-value", + "serde_json", + "serde_path_to_error", + "serde_plain", + "serde_with", + "sha2", + "subtle", + "thiserror 1.0.69", + "url", +] + [[package]] name = "openssl" version = "0.10.76" @@ -6552,7 +7089,7 @@ dependencies = [ "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost", + "prost 0.14.3", "reqwest 0.13.2", "thiserror 2.0.18", "tokio", @@ -6567,7 +7104,7 @@ source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=345cd74a# dependencies = [ "opentelemetry", "opentelemetry_sdk", - "prost", + "prost 0.14.3", "tonic", "tonic-prost", ] @@ -6604,6 +7141,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -6620,6 +7166,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "par-core" version = "2.0.0" @@ -6669,6 +7239,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -6710,10 +7291,20 @@ checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest", "hmac", - "password-hash", + "password-hash 0.4.2", "sha2", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pctx_code_execution_runtime" version = "0.2.0" @@ -6934,6 +7525,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.13.0", +] + [[package]] name = "phf" version = "0.11.3" @@ -7019,6 +7620,21 @@ dependencies = [ "spki", ] +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2 0.12.2", + "scrypt", + "sha2", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -7026,6 +7642,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", + "pkcs5", + "rand_core 0.6.4", "spki", ] @@ -7080,6 +7698,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -7088,9 +7717,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -7176,6 +7805,15 @@ dependencies = [ "num-integer", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -7200,6 +7838,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -7223,6 +7883,16 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive 0.13.5", +] + [[package]] name = "prost" version = "0.14.3" @@ -7230,7 +7900,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.14.3", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost 0.13.5", + "prost-types 0.13.5", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -7246,13 +7949,80 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "prost-reflect" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5edd582b62f5cde844716e66d92565d7faf7ab1445c8cebce6e00fba83ddb2" +dependencies = [ + "base64 0.22.1", + "once_cell", + "prost 0.13.5", + "prost-reflect-derive 0.14.0", + "prost-types 0.13.5", + "serde", + "serde-value", +] + +[[package]] +name = "prost-reflect" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37587d5a8a1b3dc9863403d084fc2254b91ab75a702207098837950767e2260b" +dependencies = [ + "prost 0.13.5", + "prost-reflect-derive 0.15.1", + "prost-types 0.13.5", +] + +[[package]] +name = "prost-reflect-build" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8db7191445b1dbee19df4f6b6294e5123aef52620b344a630bb845d302622a" +dependencies = [ + "prost-build", + "prost-reflect 0.15.3", +] + +[[package]] +name = "prost-reflect-derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fce6b22f15cc8d8d400a2b98ad29202b33bd56c7d9ddd815bc803a807ecb65" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-reflect-derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab076798900edeaf1499ed1c30097db86e6697c5d02660a63d72fe4ebdcfefd2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost 0.13.5", +] + [[package]] name = "prost-types" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" dependencies = [ - "prost", + "prost 0.14.3", ] [[package]] @@ -7696,7 +8466,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" dependencies = [ - "ahash", + "ahash 0.8.12", "fluent-uri", "once_cell", "parking_lot", @@ -7758,6 +8528,7 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", @@ -7769,12 +8540,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls 0.26.4", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams 0.4.2", "web-sys", "webpki-roots 1.0.6", ] @@ -7823,7 +8596,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -7837,6 +8610,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.53" @@ -8046,6 +8829,15 @@ dependencies = [ "transpose", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.38.44" @@ -8205,6 +8997,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "ryu-js" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" + [[package]] name = "ryu-js" version = "1.0.2" @@ -8268,6 +9066,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -8308,6 +9115,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.2.1" @@ -8358,6 +9177,18 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash 0.5.0", + "pbkdf2 0.12.2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -8374,6 +9205,20 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -8410,12 +9255,6 @@ dependencies = [ "libc", ] -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - [[package]] name = "semver" version = "1.0.27" @@ -8450,6 +9289,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_bytes" version = "0.11.19" @@ -8525,6 +9374,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_spanned" version = "1.0.4" @@ -8562,21 +9422,30 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", "serde_core", + "serde_json", "serde_with_macros", + "time", ] [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8693,6 +9562,116 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "sigstore" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43427f0d642cfed11bd596608148ee4476dd75f938888aa13a9c4e176fe14225" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64 0.22.1", + "cfg-if", + "chrono", + "const-oid", + "crypto_secretbox", + "digest", + "ecdsa", + "ed25519", + "ed25519-dalek", + "elliptic-curve", + "futures", + "futures-util", + "hex", + "json-syntax", + "oci-client", + "olpc-cjson", + "openidconnect", + "p256", + "p384", + "pem", + "pkcs1", + "pkcs8", + "rand 0.8.5", + "regex", + "reqwest 0.12.28", + "ring", + "rsa", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "scrypt", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "sha2", + "signature", + "sigstore_protobuf_specs", + "thiserror 2.0.18", + "tls_codec", + "tokio", + "tokio-util", + "tough", + "tracing", + "url", + "webbrowser", + "x509-cert", + "zeroize", +] + +[[package]] +name = "sigstore-protobuf-specs-derive" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80baa401f274093f7bb27d7a69d6139cbc11f1b97624e9a61a9b3ea32c776a35" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sigstore-verification" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db10507b7d2ff109e56bfd885ce7b489a16040996516020ecfb43ced017a7a47" +dependencies = [ + "async-trait", + "base64 0.22.1", + "ed25519-dalek", + "hex", + "log", + "p256", + "p384", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "signature", + "sigstore", + "thiserror 2.0.18", + "tokio", + "x509-parser", +] + +[[package]] +name = "sigstore_protobuf_specs" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e5ed827a6d8d2be7fc598515d061b59d85f496d7066152822a80f3250af74" +dependencies = [ + "anyhow", + "glob", + "prost 0.13.5", + "prost-build", + "prost-reflect 0.14.7", + "prost-reflect-build", + "prost-types 0.13.5", + "serde", + "serde_json", + "sigstore-protobuf-specs-derive", + "which 7.0.3", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -8755,6 +9734,16 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallstr" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862077b1e764f04c251fe82a2ef562fd78d7cadaeb072ca7c2bcaf7217b1ff3b" +dependencies = [ + "serde", + "smallvec", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -8781,6 +9770,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "futures-core", + "pin-project", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "socket2" version = "0.5.10" @@ -9285,7 +10297,7 @@ dependencies = [ "once_cell", "regex", "rustc-hash 2.1.1", - "ryu-js", + "ryu-js 1.0.2", "serde", "swc_allocator", "swc_atoms", @@ -9483,7 +10495,7 @@ dependencies = [ "once_cell", "par-core", "rustc-hash 2.1.1", - "ryu-js", + "ryu-js 1.0.2", "swc_atoms", "swc_common", "swc_ecma_ast", @@ -10212,9 +11224,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -10225,13 +11237,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tokenizers" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" dependencies = [ - "ahash", + "ahash 0.8.12", "aho-corasick", "compact_str 0.9.0", "dary_heap", @@ -10375,20 +11408,20 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" dependencies = [ "serde_core", "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.0", ] [[package]] @@ -10402,27 +11435,27 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tonic" @@ -10460,7 +11493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" dependencies = [ "bytes", - "prost", + "prost 0.14.3", "tonic", ] @@ -10470,11 +11503,46 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a875a902255423d34c1f20838ab374126db8eb41625b7947a1d54113b0b7399" dependencies = [ - "prost", - "prost-types", + "prost 0.14.3", + "prost-types 0.14.3", "tonic", ] +[[package]] +name = "tough" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88d0ee9525696569cc2af5d46f8a739028c0268895071e0386957195b0c9161" +dependencies = [ + "async-recursion", + "async-trait", + "aws-lc-rs", + "bytes", + "chrono", + "dyn-clone", + "futures", + "futures-core", + "globset", + "hex", + "log", + "olpc-cjson", + "pem", + "percent-encoding", + "reqwest 0.12.28", + "rustls 0.23.37", + "serde", + "serde_json", + "serde_plain", + "snafu", + "tempfile", + "tokio", + "tokio-util", + "typed-path 0.9.3", + "untrusted 0.7.1", + "url", + "walkdir", +] + [[package]] name = "tower" version = "0.5.3" @@ -10657,9 +11725,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.6" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13f456d2108c3fef07342ba4689a8503ec1fb5beed245e2b9be93096ef394848" +checksum = "e7a6592b1aec0109df37b6bafea77eb4e61466e37b0a5a98bef4f89bfb81b7a2" dependencies = [ "cc", "regex", @@ -10806,6 +11874,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" +[[package]] +name = "typed-path" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82205ffd44a9697e34fc145491aa47310f9871540bb7909eaa9365e0a9a46607" + [[package]] name = "typed-path" version = "0.12.3" @@ -10885,7 +11959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "408c7e039c96ec1d517a1111ade7fadab889f32c096dac691a1e3b8018c3e39a" dependencies = [ "aes", - "ahash", + "ahash 0.8.12", "base64 0.22.1", "byteorder", "cbc", @@ -11003,6 +12077,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -11046,6 +12130,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-decode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -11312,6 +12402,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -11374,7 +12477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" dependencies = [ "core-foundation 0.10.1", - "jni 0.22.3", + "jni 0.22.4", "log", "ndk-context", "objc2", @@ -11428,6 +12531,18 @@ dependencies = [ "winsafe", ] +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix 1.1.4", + "winsafe", +] + [[package]] name = "which" version = "8.0.2" @@ -11954,6 +13069,12 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" dependencies = [ "memchr", ] @@ -12100,6 +13221,37 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "sha1", + "signature", + "spki", + "tls_codec", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -12310,7 +13462,7 @@ dependencies = [ "crossbeam-utils", "flate2", "hmac", - "pbkdf2", + "pbkdf2 0.11.0", "sha1", "time", "zstd 0.11.2+zstd.1.5.2", @@ -12342,7 +13494,7 @@ dependencies = [ "crc32fast", "indexmap 2.13.0", "memchr", - "typed-path", + "typed-path 0.12.3", ] [[package]] @@ -12355,7 +13507,7 @@ dependencies = [ "flate2", "indexmap 2.13.0", "memchr", - "typed-path", + "typed-path 0.12.3", "zopfli", ] diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index d85ec6c4b6..a69c702cbb 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -61,6 +61,8 @@ url = { workspace = true } urlencoding = { workspace = true } clap_complete = "4.5.62" comfy-table = "7.2.2" +sha2 = "0.10" +sigstore-verification = { version = "0.1", default-features = false, features = ["rustls"] } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["wincred"] } diff --git a/crates/goose-cli/src/commands/update.rs b/crates/goose-cli/src/commands/update.rs index 7f0afc811d..4688d8177b 100644 --- a/crates/goose-cli/src/commands/update.rs +++ b/crates/goose-cli/src/commands/update.rs @@ -1,6 +1,8 @@ use anyhow::{bail, Context, Result}; +use sha2::{Digest, Sha256}; use std::env; use std::fs; +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; @@ -40,10 +42,90 @@ fn binary_name() -> &'static str { } } +// --------------------------------------------------------------------------- +// Sigstore / SLSA provenance verification +// --------------------------------------------------------------------------- + +/// Compute the SHA-256 hex digest of a byte slice. +fn sha256_hex(data: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(data); + format!("{:x}", hasher.finalize()) +} + +/// Verify the downloaded archive against GitHub's Sigstore SLSA provenance. +/// +/// Uses the attestations published by `actions/attest-build-provenance` in the +/// release/canary/nightly workflows (added in PR #7097). Verification fetches +/// the attestation bundle from GitHub's attestation API and validates the +/// Sigstore signature chain, Rekor transparency log inclusion, and artifact +/// digest match. +/// Returns `Ok(true)` when the attestation is fully verified, `Ok(false)` when +/// verification was skipped with a warning (no attestation, transient error), +/// and `Err` when verification actively fails (invalid signature, digest +/// mismatch, etc.). +async fn verify_provenance(archive_data: &[u8], tag: &str) -> Result { + let digest = sha256_hex(archive_data); + println!("Archive SHA-256: {digest}"); + + let mut tmp_file = tempfile::NamedTempFile::new() + .context("Failed to create temp file for provenance verification")?; + tmp_file + .write_all(archive_data) + .context("Failed to write archive to temp file")?; + tmp_file.flush().context("Failed to flush temp file")?; + + let workflow = match tag { + "canary" => "canary.yml", + _ => "release.yml", + }; + + let token = std::env::var("GITHUB_TOKEN") + .ok() + .or_else(|| std::env::var("GH_TOKEN").ok()); + + println!("Verifying SLSA provenance via Sigstore..."); + + match sigstore_verification::verify_github_attestation( + tmp_file.path(), + "block", + "goose", + token.as_deref(), + Some(workflow), + ) + .await + { + Ok(_) => { + println!("Sigstore provenance verification passed."); + Ok(true) + } + Err(sigstore_verification::AttestationError::NoAttestations) => { + eprintln!( + "Warning: No Sigstore attestation found for this build. \ + This may be expected for canary or nightly builds." + ); + Ok(false) + } + Err(sigstore_verification::AttestationError::Verification(msg)) => Err(anyhow::anyhow!( + "Sigstore verification failed: {}\n\nAborting update due to security check failure.", + msg + )), + Err(e) => { + eprintln!( + "Warning: Sigstore provenance check could not complete: {e}\n\ + This may be expected for releases published before provenance \ + attestations were enabled." + ); + Ok(false) + } + } +} + /// Update the goose binary to the latest release. /// -/// Downloads the platform-appropriate archive from GitHub releases, -/// extracts it, and replaces the current binary in-place. +/// Downloads the platform-appropriate archive from GitHub releases, verifies +/// its SLSA provenance via Sigstore, extracts it with path-traversal +/// hardening, and replaces the current binary in-place. pub async fn update(canary: bool, reconfigure: bool) -> Result<()> { #[cfg(feature = "disable-update")] { @@ -78,7 +160,10 @@ pub async fn update(canary: bool, reconfigure: bool) -> Result<()> { println!("Downloaded {} bytes.", bytes.len()); - // --- Extract to temp dir ------------------------------------------------ + // --- Verify SLSA provenance via Sigstore -------------------------------- + let provenance_verified = verify_provenance(&bytes, tag).await?; + + // --- Extract to temp dir (hardened against path traversal) -------------- let tmp_dir = tempfile::tempdir().context("Failed to create temp directory")?; #[cfg(target_os = "windows")] @@ -103,7 +188,11 @@ pub async fn update(canary: bool, reconfigure: bool) -> Result<()> { #[cfg(target_os = "windows")] copy_dlls(&extracted_binary, ¤t_exe)?; - println!("goose updated successfully!"); + if provenance_verified { + println!("goose updated successfully (verified with Sigstore SLSA provenance)."); + } else { + println!("goose updated successfully."); + } // --- Reconfigure if requested ------------------------------------------- if reconfigure { @@ -125,27 +214,94 @@ pub async fn update(canary: bool, reconfigure: bool) -> Result<()> { // Archive extraction // --------------------------------------------------------------------------- -/// Extract a .zip archive (Windows). +/// Extract a .zip archive with path-traversal hardening (Windows). +/// +/// Iterates entries individually and uses `enclosed_name()` to reject any +/// path that escapes the destination directory (zip-slip protection). #[cfg(target_os = "windows")] fn extract_zip(data: &[u8], dest: &Path) -> Result<()> { use std::io::Cursor; let cursor = Cursor::new(data); let mut archive = zip::ZipArchive::new(cursor).context("Failed to open zip archive")?; - archive - .extract(dest) - .context("Failed to extract zip archive")?; + + for i in 0..archive.len() { + let mut entry = archive + .by_index(i) + .with_context(|| format!("Failed to read zip entry at index {i}"))?; + + let safe_path = match entry.enclosed_name() { + Some(p) => p.to_owned(), + None => bail!("Zip entry has unsafe path: {}", entry.name()), + }; + + let target = dest.join(&safe_path); + + if entry.is_dir() { + fs::create_dir_all(&target)?; + } else { + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + let mut out = fs::File::create(&target)?; + std::io::copy(&mut entry, &mut out)?; + } + } + Ok(()) } -/// Extract a .tar.bz2 archive (macOS / Linux). +/// Validate that an archive entry path is safe (no absolute paths, no `..`). +fn validate_entry_path(path: &Path) -> Result<()> { + if path.is_absolute() { + bail!("Tar entry has absolute path: {}", path.display()); + } + for component in path.components() { + if matches!(component, std::path::Component::ParentDir) { + bail!("Tar entry contains path traversal: {}", path.display()); + } + } + Ok(()) +} + +/// Extract a .tar.bz2 archive with path-traversal hardening (macOS / Linux). +/// +/// Iterates entries individually, rejecting any entry whose path is absolute +/// or contains `..` components (tar-slip protection). #[cfg(not(target_os = "windows"))] fn extract_tar_bz2(data: &[u8], dest: &Path) -> Result<()> { use bzip2::read::BzDecoder; let decoder = BzDecoder::new(data); let mut archive = tar::Archive::new(decoder); - archive - .unpack(dest) - .context("Failed to extract tar.bz2 archive")?; + + for entry in archive.entries().context("Failed to read tar entries")? { + let mut entry = entry.context("Failed to read tar entry")?; + let path = entry + .path() + .context("Failed to read entry path")? + .into_owned(); + + validate_entry_path(&path)?; + + // Block symlinks and hardlinks whose targets escape the destination directory. + // Use entry.link_name() (not entry.header().link_name()) so GNU/PAX extended + // metadata (linkpath) is resolved; the header field alone may be truncated. + let link_target_opt = entry + .link_name() + .context("Failed to read link name from tar entry")?; + if let Some(link_target) = link_target_opt { + validate_entry_path(&link_target)?; + } + + let target = dest.join(&path); + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + + entry + .unpack(&target) + .with_context(|| format!("Failed to extract: {}", path.display()))?; + } + Ok(()) } @@ -457,4 +613,143 @@ mod tests { // DLL should be in goose-package too assert!(tmp.path().join("goose-package/libtest.dll").exists()); } + + // ----------------------------------------------------------------------- + // SHA-256 digest tests + // ----------------------------------------------------------------------- + + #[test] + fn test_sha256_hex_known_value() { + let digest = sha256_hex(b"hello world"); + assert_eq!( + digest, + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + ); + } + + #[test] + fn test_sha256_hex_empty() { + let digest = sha256_hex(b""); + assert_eq!( + digest, + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ); + } + + // ----------------------------------------------------------------------- + // Path validation and extraction hardening tests + // ----------------------------------------------------------------------- + + #[test] + fn test_validate_entry_path_accepts_safe_paths() { + assert!(validate_entry_path(Path::new("goose")).is_ok()); + assert!(validate_entry_path(Path::new("goose-package/goose")).is_ok()); + assert!(validate_entry_path(Path::new("subdir/nested/file.txt")).is_ok()); + } + + #[test] + fn test_validate_entry_path_rejects_absolute() { + let result = validate_entry_path(Path::new("/etc/malicious")); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("absolute path")); + } + + #[test] + fn test_validate_entry_path_rejects_traversal() { + let result = validate_entry_path(Path::new("../../escape.txt")); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("path traversal")); + } + + #[test] + fn test_validate_entry_path_rejects_nested_traversal() { + let result = validate_entry_path(Path::new("safe/../../escape")); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("path traversal")); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn test_extract_tar_bz2_safe_archive() { + use bzip2::write::BzEncoder; + use bzip2::Compression; + + let tmp = tempdir().unwrap(); + + let mut builder_buf = Vec::new(); + { + let encoder = BzEncoder::new(&mut builder_buf, Compression::default()); + let mut builder = tar::Builder::new(encoder); + + let data = b"goose binary content"; + let mut header = tar::Header::new_gnu(); + header.set_size(data.len() as u64); + header.set_mode(0o755); + header.set_cksum(); + builder + .append_data(&mut header, "goose-package/goose", &data[..]) + .unwrap(); + builder.into_inner().unwrap().finish().unwrap(); + } + + extract_tar_bz2(&builder_buf, tmp.path()).unwrap(); + + let extracted = tmp.path().join("goose-package/goose"); + assert!(extracted.exists()); + assert_eq!( + fs::read_to_string(extracted).unwrap(), + "goose binary content" + ); + } + + // ----------------------------------------------------------------------- + // Sigstore provenance verification test + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn test_verify_provenance_warns_on_missing_attestation() { + let result = verify_provenance(b"not a real archive", "stable").await; + // Network failures and missing attestations are soft warnings: Ok(false), not hard errors. + assert_eq!( + result.ok(), + Some(false), + "verify_provenance should return Ok(false) when attestations cannot be fetched" + ); + } + + #[cfg(not(target_os = "windows"))] + #[test] + fn test_extract_tar_bz2_blocks_symlink_escape() { + use bzip2::write::BzEncoder; + use bzip2::Compression; + + let tmp = tempdir().unwrap(); + + let mut builder_buf = Vec::new(); + { + let encoder = BzEncoder::new(&mut builder_buf, Compression::default()); + let mut builder = tar::Builder::new(encoder); + + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_mode(0o777); + header.set_cksum(); + // Symlink whose target escapes the destination directory. + builder + .append_link(&mut header, "evil_link", "../../etc/passwd") + .unwrap(); + builder.into_inner().unwrap().finish().unwrap(); + } + + let result = extract_tar_bz2(&builder_buf, tmp.path()); + assert!( + result.is_err(), + "extraction should fail when a symlink target escapes the destination" + ); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("path traversal"), + "error should mention path traversal, got: {err_msg}" + ); + } } diff --git a/crates/goose/src/recipe/template_recipe.rs b/crates/goose/src/recipe/template_recipe.rs index f127c4c660..37dbdc1c3b 100644 --- a/crates/goose/src/recipe/template_recipe.rs +++ b/crates/goose/src/recipe/template_recipe.rs @@ -147,12 +147,16 @@ fn get_env_with_template_variables( undefined_behavior: UndefinedBehavior, ) -> Result<(Environment<'_>, HashSet)> { let env = add_template_in_env(content, recipe_dir, undefined_behavior)?; - let template = env.get_template(CURRENT_TEMPLATE_NAME).unwrap(); - let state = template.eval_to_state(())?; - let mut template_variables = HashSet::new(); - for (_, template) in state.env().templates() { - template_variables.extend(template.undeclared_variables(true)); - } + let template_variables = { + let template = env.get_template(CURRENT_TEMPLATE_NAME).unwrap(); + let captured = template.render_captured(())?; + let state = captured.state(); + let mut vars = HashSet::new(); + for (_, tmpl) in state.env().templates() { + vars.extend(tmpl.undeclared_variables(true)); + } + vars + }; Ok((env, template_variables)) } From 53236ff327d292fd80bf7746e7097e07107b3ab4 Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Thu, 19 Mar 2026 15:06:50 -0400 Subject: [PATCH 6/6] Allow delete all types (#8018) Signed-off-by: Douwe Osinga Co-authored-by: Douwe Osinga --- crates/goose-cli/src/commands/session.rs | 27 +++++++---------- crates/goose/src/session/session_manager.rs | 33 ++++++++++++++------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 5e2df3913c..769ec523b8 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -89,25 +89,18 @@ pub async fn handle_session_remove( regex_string: Option, ) -> Result<()> { let session_manager = SessionManager::instance(); - let all_sessions = match session_manager.list_sessions().await { - Ok(sessions) => sessions, - Err(e) => { - tracing::error!("Failed to retrieve sessions: {:?}", e); - return Err(anyhow::anyhow!("Failed to retrieve sessions")); - } - }; let matched_sessions: Vec; if let Some(id_val) = session_id { - if let Some(session) = all_sessions.iter().find(|s| s.id == id_val) { - matched_sessions = vec![session.clone()]; - } else { - return Err(anyhow::anyhow!("Session ID '{}' not found.", id_val)); + match session_manager.get_session(&id_val, false).await { + Ok(session) => matched_sessions = vec![session], + Err(_) => return Err(anyhow::anyhow!("Session ID '{}' not found.", id_val)), } } else if let Some(name_val) = name { - if let Some(session) = all_sessions.iter().find(|s| s.name == name_val) { - matched_sessions = vec![session.clone()]; + let all_sessions = session_manager.list_all_sessions().await?; + if let Some(session) = all_sessions.into_iter().find(|s| s.name == name_val) { + matched_sessions = vec![session]; } else { return Err(anyhow::anyhow!( "Session with name '{}' not found.", @@ -118,7 +111,8 @@ pub async fn handle_session_remove( let session_regex = Regex::new(®ex_val) .with_context(|| format!("Invalid regex pattern '{}'", regex_val))?; - matched_sessions = all_sessions + let visible_sessions = session_manager.list_sessions().await?; + matched_sessions = visible_sessions .into_iter() .filter(|session| session_regex.is_match(&session.id)) .collect(); @@ -128,10 +122,11 @@ pub async fn handle_session_remove( return Ok(()); } } else { - if all_sessions.is_empty() { + let visible_sessions = session_manager.list_sessions().await?; + if visible_sessions.is_empty() { return Err(anyhow::anyhow!("No sessions found.")); } - matched_sessions = prompt_interactive_session_removal(&all_sessions)?; + matched_sessions = prompt_interactive_session_removal(&visible_sessions)?; } if matched_sessions.is_empty() { diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index d948337319..a86465b199 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -311,7 +311,11 @@ impl SessionManager { } pub async fn list_sessions_by_types(&self, types: &[SessionType]) -> Result> { - self.storage.list_sessions_by_types(types).await + self.storage.list_sessions_by_types(Some(types)).await + } + + pub async fn list_all_sessions(&self) -> Result> { + self.storage.list_sessions_by_types(None).await } pub async fn delete_session(&self, id: &str) -> Result<()> { @@ -1239,12 +1243,19 @@ impl SessionStorage { Self::replace_conversation_inner(pool, session_id, conversation).await } - async fn list_sessions_by_types(&self, types: &[SessionType]) -> Result> { - if types.is_empty() { - return Ok(Vec::new()); - } + async fn list_sessions_by_types(&self, types: Option<&[SessionType]>) -> Result> { + let (where_clause, binds): (String, Vec) = match types { + Some(t) if !t.is_empty() => { + let placeholders: String = t.iter().map(|_| "?").collect::>().join(", "); + ( + format!("WHERE s.session_type IN ({})", placeholders), + t.iter().map(|t| t.to_string()).collect(), + ) + } + Some(_) => return Ok(Vec::new()), + None => (String::new(), Vec::new()), + }; - let placeholders: String = types.iter().map(|_| "?").collect::>().join(", "); let query = format!( r#" SELECT s.id, s.working_dir, s.name, s.description, s.user_set_name, s.session_type, s.created_at, s.updated_at, s.extension_data, @@ -1255,16 +1266,16 @@ impl SessionStorage { COUNT(m.id) as message_count FROM sessions s INNER JOIN messages m ON s.id = m.session_id - WHERE s.session_type IN ({}) + {} GROUP BY s.id ORDER BY s.updated_at DESC "#, - placeholders + where_clause ); let mut q = sqlx::query_as::<_, Session>(&query); - for t in types { - q = q.bind(t.to_string()); + for b in &binds { + q = q.bind(b); } let pool = self.pool().await?; @@ -1272,7 +1283,7 @@ impl SessionStorage { } async fn list_sessions(&self) -> Result> { - self.list_sessions_by_types(&[SessionType::User, SessionType::Scheduled]) + self.list_sessions_by_types(Some(&[SessionType::User, SessionType::Scheduled])) .await }