Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9f08b8b
chore: bump workspace dependencies
jh-block Mar 4, 2026
ae79c39
chore: disable default features for image crate to reduce dep tree
jh-block Mar 4, 2026
82c4cb1
chore: bump tiktoken-rs, hf-hub, dirs, cliclack to reduce duplicate deps
jh-block Mar 4, 2026
d20ec2a
style: cargo fmt
jh-block Mar 4, 2026
6c1dfa1
fix: use distinct names for OpenAPI proxy schemas
jh-block Mar 4, 2026
c707fc9
fix: bind Message.role to Role schema instead of String
jh-block Mar 5, 2026
cafe477
fix: restore OpenAPI discriminator metadata for tagged enums
jh-block Mar 5, 2026
9c635f2
fix: restore binary schema for diagnostics ZIP response
jh-block Mar 5, 2026
6b6faf5
fix: resolve ErrorResponse schema-name collision between tunnel and s…
jh-block Mar 5, 2026
85d53e2
refactor: remove SchemaFixups post-processing modifier
jh-block Mar 5, 2026
940ea78
fix: restore binary schema for diagnostics response to fix TS typecheck
jh-block Mar 5, 2026
d456c23
fix: re-enable PNG codec for goose-mcp image handling
jh-block Mar 5, 2026
ff7b8b2
Merge remote-tracking branch 'origin/main' into jhugo/bump-workspace-…
jh-block Mar 9, 2026
7e213d5
chore: cargo update (105 semver-compatible bumps)
jh-block Mar 9, 2026
78a6ae6
fix: restore all image format codecs for docx add_image
jh-block Mar 9, 2026
e1706c2
chore: use image default features (rayon already in dep tree)
jh-block Mar 9, 2026
604bc66
Merge remote-tracking branch 'origin/main' into jhugo/bump-workspace-…
jh-block Mar 10, 2026
84b045a
chore: remove no-op schema collision test
jh-block Mar 10, 2026
cd458c2
style: cargo fmt
jh-block Mar 10, 2026
9406b8e
Merge remote-tracking branch 'origin/main' into jhugo/bump-workspace-…
jh-block Mar 11, 2026
5080cd9
fix: use distinct schema name for ContentBlock proxy
jh-block Mar 11, 2026
2a41b22
fix: restore OpenAPI discriminators and fix self-referential schemas
Mar 26, 2026
159512f
Merge origin/main into jhugo/bump-workspace-deps
Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
466 changes: 320 additions & 146 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ base64 = "0.22.1"
bytes = "1"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4", features = ["derive"] }
dirs = "5.0"
dirs = "6.0"
dotenvy = "0.15"
env-lock = "1.0.1"
etcetera = "0.11.0"
Expand All @@ -43,7 +43,7 @@ include_dir = "0.7.4"
indoc = "2.0"
lru = "0.16"
once_cell = "1.20"
rand = "0.8"
rand = "0.10"
regex = "1.12"
reqwest = { version = "0.13", default-features = false, features = ["multipart", "form"] }
schemars = { default-features = false, version = "1.0" }
Expand All @@ -53,7 +53,7 @@ serde_yaml = "0.9"
shellexpand = "3.1"
strum = { version = "0.27", features = ["derive"] }
tempfile = "3"
thiserror = "1.0"
thiserror = "2.0"
tokio = { version = "1.49", features = ["full"] }
tokio-stream = "0.1"
tokio-util = "0.7"
Expand All @@ -62,13 +62,13 @@ tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = "0.3"
urlencoding = "2.1"
utoipa = "4.1"
utoipa = "5"
uuid = { version = "1.11", features = ["v4"] }
webbrowser = "1.0"
which = "8.0.0"
winapi = { version = "0.3", features = ["wincred"] }
wiremock = "0.6"
zip = { version = "^8.0", default-features = false, features = ["deflate"] }
zip = { version = "8", default-features = false, features = ["deflate"] }
serial_test = "3.2.0"
sha2 = "0.10"
shell-words = "1.1.1"
Expand Down
8 changes: 4 additions & 4 deletions crates/goose-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ goose-acp = { path = "../goose-acp", default-features = false }
goose-mcp = { path = "../goose-mcp" }
rmcp = { workspace = true }
clap = { workspace = true }
cliclack = "0.3.5"
cliclack = "0.4.0"
console = "0.16.1"
dotenvy = { workspace = true }
bat = { version = "0.26.1", default-features = false, features = ["regex-onig"] }
Expand All @@ -48,9 +48,9 @@ shlex = "1.3.0"
async-trait = { workspace = true }
base64 = { workspace = true }
regex = { workspace = true }
tar = "0.4.45"
reqwest = { workspace = true, features = ["blocking"], default-features = false }
zip = { workspace = true }
tar = "0.4"
reqwest = { workspace = true, features = ["blocking", "rustls"], default-features = false }
zip = { workspace = true, features = ["deflate"] }
bzip2 = "0.5"
webbrowser = { workspace = true }
indicatif = "0.18.1"
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-cli/src/session/thinking.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rand::seq::SliceRandom;
use rand::seq::IndexedRandom;

/// Extended list of playful thinking messages including both goose and general AI actions
const THINKING_MESSAGES: &[&str] = &[
Expand Down Expand Up @@ -215,6 +215,6 @@ const THINKING_MESSAGES: &[&str] = &[
/// Returns a random thinking message from the extended list
pub fn get_random_thinking_message() -> &'static str {
THINKING_MESSAGES
.choose(&mut rand::thread_rng())
.choose(&mut rand::rng())
.unwrap_or(&THINKING_MESSAGES[0])
}
2 changes: 1 addition & 1 deletion crates/goose-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ include_dir = { workspace = true }
once_cell = { workspace = true }
lopdf = "0.36.0"
docx-rs = "0.4.7"
image = { version = "0.24.9", features = ["jpeg"] }
image = "0.25"
umya-spreadsheet = "2.2.3"
shell-words = { workspace = true }
98 changes: 79 additions & 19 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use rmcp::model::{
RawContent, RawEmbeddedResource, RawImageContent, RawResource, RawTextContent,
ResourceContents, Role, TaskSupport, TextContent, Tool, ToolAnnotations, ToolExecution,
};
use utoipa::{OpenApi, ToSchema};
use utoipa::{Modify, OpenApi, PartialSchema, ToSchema};

use goose::config::declarative_providers::{
DeclarativeProviderConfig, EnvVarConfig, LoadedProvider, ProviderEngine,
Expand All @@ -29,42 +29,44 @@ use crate::routes::recipe_utils::RecipeManifest;
use crate::routes::reply::MessageEvent;
use utoipa::openapi::schema::{
AdditionalProperties, AnyOfBuilder, ArrayBuilder, ObjectBuilder, OneOfBuilder, Schema,
SchemaFormat, SchemaType,
SchemaFormat, SchemaType, Type,
};
use utoipa::openapi::{AllOfBuilder, Ref, RefOr};

macro_rules! derive_utoipa {
($inner_type:ident as $schema_name:ident) => {
pub struct $schema_name {}

impl<'__s> ToSchema<'__s> for $schema_name {
fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
impl utoipa::PartialSchema for $schema_name {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
let settings = rmcp::schemars::generate::SchemaSettings::openapi3();
let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<$inner_type>();
let schema = convert_schemars_to_utoipa(schema);
(stringify!($inner_type), schema)
convert_schemars_to_utoipa(schema)
}
}

fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> {
Vec::new()
impl ToSchema for $schema_name {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(stringify!($inner_type))
}
}
};
($inner_type:ident as $schema_name:ident => $output_name:expr) => {
pub struct $schema_name {}

impl<'__s> ToSchema<'__s> for $schema_name {
fn schema() -> (&'__s str, utoipa::openapi::RefOr<utoipa::openapi::Schema>) {
impl utoipa::PartialSchema for $schema_name {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::Schema> {
let settings = rmcp::schemars::generate::SchemaSettings::openapi3();
let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<$inner_type>();
let schema = convert_schemars_to_utoipa(schema);
($output_name, schema)
convert_schemars_to_utoipa(schema)
}
}

fn aliases() -> Vec<(&'__s str, utoipa::openapi::schema::Schema)> {
Vec::new()
impl ToSchema for $schema_name {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed($output_name)
}
}
};
Expand Down Expand Up @@ -248,7 +250,8 @@ fn convert_typed_schema(
RefOr::T(Schema::Array(array_builder.build()))
}
"string" => {
let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::String);
let mut object_builder =
ObjectBuilder::new().schema_type(SchemaType::Type(Type::String));

if let Some(Value::Array(enum_values)) = obj.get("enum") {
let values: Vec<serde_json::Value> = enum_values
Expand Down Expand Up @@ -286,7 +289,8 @@ fn convert_typed_schema(
RefOr::T(Schema::Object(object_builder.build()))
}
"number" => {
let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::Number);
let mut object_builder =
ObjectBuilder::new().schema_type(SchemaType::Type(Type::Number));

if let Some(Value::Number(minimum)) = obj.get("minimum") {
if let Some(min) = minimum.as_f64() {
Expand Down Expand Up @@ -317,7 +321,8 @@ fn convert_typed_schema(
RefOr::T(Schema::Object(object_builder.build()))
}
"integer" => {
let mut object_builder = ObjectBuilder::new().schema_type(SchemaType::Integer);
let mut object_builder =
ObjectBuilder::new().schema_type(SchemaType::Type(Type::Integer));

if let Some(Value::Number(minimum)) = obj.get("minimum") {
if let Some(min) = minimum.as_f64() {
Expand Down Expand Up @@ -349,11 +354,13 @@ fn convert_typed_schema(
}
"boolean" => RefOr::T(Schema::Object(
ObjectBuilder::new()
.schema_type(SchemaType::Boolean)
.schema_type(SchemaType::Type(Type::Boolean))
.build(),
)),
"null" => RefOr::T(Schema::Object(
ObjectBuilder::new().schema_type(SchemaType::String).build(),
ObjectBuilder::new()
.schema_type(SchemaType::Type(Type::String))
.build(),
)),
_ => RefOr::T(Schema::Object(ObjectBuilder::new().build())),
}
Expand All @@ -379,8 +386,61 @@ derive_utoipa!(ResourceContents as ResourceContentsSchema);
derive_utoipa!(JsonObject as JsonObjectSchema);
derive_utoipa!(Icon as IconSchema);

struct OpenApiFixups;

impl Modify for OpenApiFixups {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
use utoipa::openapi::schema::{Discriminator, Schema};
use utoipa::openapi::RefOr;

// Fix diagnostics endpoint: Vec<u8> renders as array<integer> but we need binary
if let Some(path) = openapi.paths.paths.get_mut("/diagnostics/{session_id}") {
if let Some(op) = &mut path.get {
if let Some(RefOr::T(resp)) = op.responses.responses.get_mut("200") {
if let Some(content) = resp.content.get_mut("application/zip") {
content.schema = Some(RefOr::T(Schema::Object(
ObjectBuilder::new()
.schema_type(SchemaType::new(Type::String))
.format(Some(SchemaFormat::KnownFormat(
utoipa::openapi::schema::KnownFormat::Binary,
)))
.build(),
)));
}
}
}
}

if let Some(components) = &mut openapi.components {
// Proxy types used via value_type overwrite real schemas with self-refs.
// Re-insert the authoritative schemas from derive_utoipa! macros.
let real_schemas: Vec<(&str, RefOr<Schema>)> = vec![
("TextContent", TextContentSchema::schema()),
("ImageContent", ImageContentSchema::schema()),
("Role", RoleSchema::schema()),
("ContentBlock", ContentBlockSchema::schema()),
];
for (name, schema) in real_schemas {
components.schemas.insert(name.to_string(), schema);
}

// Restore discriminators for tagged enums (utoipa 5 drops them)
for (name, property_name) in [
("MessageContent", "type"),
("ActionRequiredData", "actionType"),
("ExtensionConfig", "type"),
Comment on lines +428 to +431
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore discriminator metadata for all tagged oneOf schemas

The new OpenApiFixups whitelist only restores discriminators for three schemas, so other tagged enums that previously carried discriminator metadata now lose it (e.g. MessageEvent, ModelDownloadStatus, SamplingConfig, and SuccessCheck in ui/desktop/openapi.json). This is a contract regression introduced by the utoipa 5 migration: OpenAPI client generators that depend on discriminator.propertyName for polymorphic deserialization can no longer reliably map these oneOf payloads back to concrete variants.

Useful? React with 👍 / 👎.

] {
if let Some(RefOr::T(Schema::OneOf(one_of))) = components.schemas.get_mut(name) {
one_of.discriminator = Some(Discriminator::new(property_name));
}
}
}
}
}

#[derive(OpenApi)]
#[openapi(
modifiers(&OpenApiFixups),
paths(
super::routes::status::status,
super::routes::status::system_info,
Expand Down
24 changes: 12 additions & 12 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,25 +141,25 @@ pub struct CallToolRequest {
arguments: Value,
}

/// Ref-only alias so utoipa emits `$ref: "#/components/schemas/ContentBlock"`.
/// The actual schema is registered via `derive_utoipa!(RawContent as ContentBlockSchema => "ContentBlock")`.
#[allow(dead_code)]
pub enum ContentBlock {}

impl<'s> utoipa::ToSchema<'s> for ContentBlock {
fn schema() -> (
&'s str,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
) {
// Delegate to the auto-generated schema
crate::openapi::ContentBlockSchema::schema()
pub enum ContentBlockRef {}

impl utoipa::PartialSchema for ContentBlockRef {
fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
utoipa::openapi::RefOr::Ref(utoipa::openapi::Ref::from_schema_name("ContentBlock"))
}
}

impl utoipa::ToSchema for ContentBlockRef {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("ContentBlock")
}
Comment thread
jh-block marked this conversation as resolved.
}

#[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResponse {
#[schema(value_type = Vec<ContentBlock>)]
#[schema(value_type = Vec<ContentBlockRef>)]
content: Vec<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
structured_content: Option<Value>,
Expand Down
17 changes: 3 additions & 14 deletions crates/goose-server/src/routes/tunnel.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
use crate::routes::errors::ErrorResponse;
use crate::state::AppState;
use crate::tunnel::TunnelInfo;
use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use serde::Serialize;
use std::sync::Arc;
use utoipa::ToSchema;

#[derive(Debug, Serialize, ToSchema)]
pub struct ErrorResponse {
pub error: String,
}

/// Start the tunnel
#[utoipa::path(
Expand All @@ -31,13 +26,7 @@ pub async fn start_tunnel(State(state): State<Arc<AppState>>) -> Response {
Ok(info) => (StatusCode::OK, Json(info)).into_response(),
Err(e) => {
tracing::error!("Failed to start tunnel: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: e.to_string(),
}),
)
.into_response()
ErrorResponse::internal(e.to_string()).into_response()
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async-trait = { workspace = true }
async-stream = { workspace = true }
minijinja = { version = "2.12.0", features = ["loader"] }
include_dir = { workspace = true }
tiktoken-rs = "0.6.0"
tiktoken-rs = "0.9.1"
chrono = { workspace = true }
clap = { workspace = true }
indoc = { workspace = true }
Expand Down Expand Up @@ -152,15 +152,14 @@ agent-client-protocol-schema = { workspace = true }
sacp = { workspace = true, features = ["unstable"] }
unicode-normalization = "0.1"

# For local Whisper transcription (optional, behind "local-inference" feature)
candle-core = { version = "0.9", default-features = false, optional = true }
candle-nn = { version = "0.9", default-features = false, optional = true }
candle-transformers = { version = "0.9", default-features = false, optional = true }
byteorder = { version = "1.5.0", optional = true }
tokenizers = { version = "0.21.0", default-features = false, features = ["onig"], optional = true }
symphonia = { version = "0.5", features = ["all"], optional = true }
rubato = { version = "0.16", optional = true }
zip = { workspace = true }
zip = { workspace = true, features = ["deflate"] }
sys-info = "0.9"

schemars = { workspace = true, features = [
Expand Down
7 changes: 7 additions & 0 deletions crates/goose/src/agents/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ use thiserror::Error;
use tracing::warn;
use utoipa::ToSchema;

fn tool_vec_schema() -> utoipa::openapi::schema::Array {
utoipa::openapi::schema::ArrayBuilder::new()
.items(utoipa::openapi::Ref::from_schema_name("Tool"))
.build()
}

pub use crate::agents::platform_extensions::{
PlatformExtensionContext, PlatformExtensionDef, PLATFORM_EXTENSIONS,
};
Expand Down Expand Up @@ -250,6 +256,7 @@ pub enum ExtensionConfig {
#[schema(required)]
description: String,
/// The tools provided by the frontend
#[schema(schema_with = tool_vec_schema)]
tools: Vec<Tool>,
/// Instructions for how to use these tools
instructions: Option<String>,
Expand Down
Loading
Loading