Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c4289ce
feat(extensions): add Unix socket transport for StreamableHttp extens…
wpfleger96 Mar 3, 2026
f4af365
fix: make TLS opt-out in goosed agent via GOOSE_TLS env var
wpfleger96 Mar 6, 2026
99564e9
fix: thread TLS scheme through TunnelManager
wpfleger96 Mar 6, 2026
9eeac23
Merge remote-tracking branch 'origin/main' into wpfleger/tls
wpfleger96 Mar 6, 2026
5716182
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 6, 2026
52ee2b5
Merge remote-tracking branch 'origin/wpfleger/tls' into wpfleger/sock…
wpfleger96 Mar 6, 2026
944f675
fix(extensions):
wpfleger96 Mar 6, 2026
53517de
fix(extensions): add OAuth retry to Unix socket client setup
wpfleger96 Mar 6, 2026
057a044
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 6, 2026
6c9dad3
fix(extensions): support Unix socket error type in OAuth auth detection
wpfleger96 Mar 6, 2026
02320d0
fix(extensions): preserve user headers on Unix socket OAuth retry
wpfleger96 Mar 6, 2026
b5487d8
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 9, 2026
c1d7153
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 9, 2026
322d0b5
fix(extensions): use spawn_blocking for abstract socket connect
wpfleger96 Mar 10, 2026
93c2b69
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 16, 2026
94b8d10
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 23, 2026
b884cd1
fix: use `std::io::Error::other()` to satisfy clippy `io_other_error`…
wpfleger96 Mar 23, 2026
9854fef
fix: resolve clippy redundant_closure and expand env vars in socket f…
wpfleger96 Mar 23, 2026
403b39b
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 23, 2026
b3c49b9
Merge remote-tracking branch 'origin/main' into wpfleger/socket-support
wpfleger96 Mar 24, 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
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/goose-acp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result<ExtensionConf
.into_iter()
.map(|h| (h.name, h.value))
.collect(),
socket: None,
timeout: None,
bundled: Some(false),
available_tools: vec![],
Expand Down Expand Up @@ -1403,6 +1404,7 @@ mod tests {
"Authorization".into(),
"Bearer ghp_xxxxxxxxxxxx".into()
)]),
socket: None,
timeout: None,
bundled: Some(false),
available_tools: vec![],
Expand Down
1 change: 1 addition & 0 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,7 @@ fn configure_streamable_http_extension() -> anyhow::Result<()> {
envs: Envs::new(envs),
env_keys,
headers,
socket: None,
description,
timeout: Some(timeout),
bundled: None,
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-cli/src/recipes/secret_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ mod tests {
bundled: None,
available_tools: Vec::new(),
headers: HashMap::new(),
socket: None,
},
ExtensionConfig::Stdio {
name: "slack-mcp".to_string(),
Expand Down Expand Up @@ -246,6 +247,7 @@ mod tests {
bundled: None,
available_tools: Vec::new(),
headers: HashMap::new(),
socket: None,
},
ExtensionConfig::Stdio {
name: "service-b".to_string(),
Expand Down Expand Up @@ -305,6 +307,7 @@ mod tests {
bundled: None,
available_tools: Vec::new(),
headers: HashMap::new(),
socket: None,
}]),
sub_recipes: Some(vec![SubRecipe {
name: "child-recipe".to_string(),
Expand Down
4 changes: 4 additions & 0 deletions crates/goose-cli/src/session/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ impl CliSession {
envs: Envs::new(HashMap::new()),
env_keys: Vec::new(),
headers: HashMap::new(),
socket: None,
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
timeout: Some(timeout),
bundled: None,
Expand Down Expand Up @@ -2036,6 +2037,7 @@ mod tests {
envs: Envs::default(),
env_keys: vec![],
headers: HashMap::new(),
socket: None,
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
timeout: Some(300),
bundled: None,
Expand All @@ -2051,6 +2053,7 @@ mod tests {
envs: Envs::default(),
env_keys: vec![],
headers: HashMap::new(),
socket: None,
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
timeout: Some(300),
bundled: None,
Expand All @@ -2066,6 +2069,7 @@ mod tests {
envs: Envs::default(),
env_keys: vec![],
headers: HashMap::new(),
socket: None,
description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(),
timeout: Some(300),
bundled: None,
Expand Down
9 changes: 9 additions & 0 deletions crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ pulldown-cmark = "0.13.0"
llama-cpp-2 = { version = "0.1.137", features = ["sampler"] }
encoding_rs = "0.8.35"

# Unix domain socket HTTP transport for StreamableHttp extensions
[target.'cfg(unix)'.dependencies]
hyper = { version = "1", features = ["client", "http1"] }
hyper-util = { version = "0.1", features = ["tokio"] }
http-body-util = "0.1"
sse-stream = "0.2"
bytes = { workspace = true }
http = { workspace = true }

[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] }

Expand Down
81 changes: 78 additions & 3 deletions crates/goose/src/agents/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ pub enum ExtensionConfig {
env_keys: Vec<String>,
#[serde(default)]
headers: HashMap<String, String>,
/// Unix domain socket path to route HTTP through (e.g. "@egress.sock" for Envoy sidecar).
/// When set, the physical connection goes through this socket while `uri` is used for the
/// HTTP Host header and path. Useful in K8s environments where DNS only resolves via Envoy.
#[serde(default)]
socket: Option<String>,
Comment thread
wpfleger96 marked this conversation as resolved.
// NOTE: set timeout to be optional for compatibility.
// However, new configurations should include this field.
timeout: Option<u64>,
Expand Down Expand Up @@ -305,6 +310,7 @@ impl ExtensionConfig {
envs: Envs::default(),
env_keys: Vec::new(),
headers: HashMap::new(),
socket: None,
description: description.into(),
timeout: Some(timeout.into()),
bundled: None,
Expand Down Expand Up @@ -459,6 +465,7 @@ impl ExtensionConfig {
envs,
env_keys,
headers,
socket,
timeout,
bundled,
available_tools,
Expand All @@ -478,6 +485,7 @@ impl ExtensionConfig {
envs: Envs::new(merged),
env_keys: vec![],
headers,
socket,
Comment on lines 485 to +488
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 Resolve socket env vars in StreamableHttp snapshots

ExtensionConfig::resolve now substitutes env-backed uri and headers, but it carries socket through unchanged. ExtensionManager::add_extension uses the resolved snapshot to detect secret rotation before deciding to skip a restart (existing.resolved_config == resolved_config in crates/goose/src/agents/extension_manager.rs:673-681), so a config like socket: $ENVOY_SOCK with env_keys: [ENVOY_SOCK] will never reconnect when the stored socket path changes. After a sidecar/socket rollover, Goose can stay pinned to a stale UDS until the whole process is restarted.

Useful? React with 👍 / 👎.

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 Resolve socket env vars in StreamableHttp::resolve

ExtensionConfig::resolve substitutes env-backed values for uri and headers but leaves socket unchanged, which means ExtensionManager::add_extension's resolved-config equality check (lines 679-681 in extension_manager.rs) can miss socket-path secret rotation and skip the restart. In environments using socket: $.../env_keys, the client can stay bound to an outdated UDS path until process restart. Fresh evidence here is the new socket field being passed through unchanged at this line in the resolve path.

Useful? React with 👍 / 👎.

timeout,
bundled,
available_tools,
Expand All @@ -494,9 +502,12 @@ impl std::fmt::Display for ExtensionConfig {
ExtensionConfig::Sse { name, .. } => {
write!(f, "SSE({}: unsupported)", name)
}
ExtensionConfig::StreamableHttp { name, uri, .. } => {
write!(f, "StreamableHttp({}: {})", name, uri)
}
ExtensionConfig::StreamableHttp {
name, uri, socket, ..
} => match socket {
Some(s) => write!(f, "StreamableHttp({}: {} via {})", name, uri, s),
None => write!(f, "StreamableHttp({}: {})", name, uri),
},
ExtensionConfig::Stdio {
name, cmd, args, ..
} => {
Expand Down Expand Up @@ -669,6 +680,7 @@ available_tools: []
)]
.into_iter()
.collect(),
socket: None,
timeout: None,
bundled: None,
available_tools: vec![],
Expand All @@ -689,6 +701,7 @@ available_tools: []
)]
.into_iter()
.collect(),
socket: None,
timeout: None,
bundled: None,
available_tools: vec![],
Expand Down Expand Up @@ -762,6 +775,7 @@ available_tools: []
)]
.into_iter()
.collect(),
socket: None,
timeout: None,
bundled: None,
available_tools: vec![],
Expand All @@ -779,6 +793,7 @@ available_tools: []
headers: [("Authorization".to_string(), "Bearer secret_value".to_string())]
.into_iter()
.collect(),
socket: None,
timeout: None,
bundled: None,
available_tools: vec![],
Expand Down Expand Up @@ -829,4 +844,64 @@ available_tools: []
cfg.set("MY_SECRET", &"secret_value", true).unwrap();
assert_eq!(config.resolve(&cfg).await.unwrap(), expected);
}

#[test]
fn test_deserialize_streamable_http_with_socket() {
let config: ExtensionConfig = serde_yaml::from_str(
"type: streamable_http\nname: ai-app-info\ndescription: test\nuri: http://example.com/mcp\nsocket: \"@egress.sock\"\n",
)
.unwrap();
if let ExtensionConfig::StreamableHttp { socket, .. } = config {
assert_eq!(socket, Some("@egress.sock".to_string()));
} else {
panic!("unexpected variant");
}
}

#[test]
fn test_deserialize_streamable_http_without_socket() {
let config: ExtensionConfig = serde_yaml::from_str(
"type: streamable_http\nname: ai-app-info\ndescription: test\nuri: http://example.com/mcp\n",
)
.unwrap();
if let ExtensionConfig::StreamableHttp { socket, .. } = config {
assert_eq!(socket, None);
} else {
panic!("unexpected variant");
}
}

#[test]
fn test_display_streamable_http_without_socket() {
let config = ExtensionConfig::streamable_http(
"ai-app-info",
"http://example.com/mcp",
"test",
300u64,
);
assert_eq!(
format!("{config}"),
"StreamableHttp(ai-app-info: http://example.com/mcp)"
);
}

#[test]
fn test_display_streamable_http_with_socket() {
let config = ExtensionConfig::StreamableHttp {
name: "ai-app-info".to_string(),
uri: "http://example.com/mcp".to_string(),
description: "test".to_string(),
timeout: Some(300),
headers: Default::default(),
envs: Default::default(),
env_keys: vec![],
socket: Some("@egress.sock".to_string()),
bundled: None,
available_tools: vec![],
};
assert_eq!(
format!("{config}"),
"StreamableHttp(ai-app-info: http://example.com/mcp via @egress.sock)"
);
}
}
Loading
Loading