fix: per session working dir isolation (#6920)

This commit is contained in:
Zane
2026-02-03 10:20:02 -08:00
committed by GitHub
parent 8631caa890
commit aa356bd460
16 changed files with 143 additions and 68 deletions
@@ -98,6 +98,7 @@ impl McpClientTrait for MockClient {
_session_id: &str, _session_id: &str,
name: &str, name: &str,
arguments: Option<serde_json::Map<String, Value>>, arguments: Option<serde_json::Map<String, Value>>,
_working_dir: Option<&str>,
_cancel_token: CancellationToken, _cancel_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
if let Some(handler) = self.handlers.get(name) { if let Some(handler) = self.handlers.get(name) {
@@ -10,13 +10,26 @@ use rmcp::{
model::{ model::{
CallToolResult, CancelledNotificationParam, Content, ErrorCode, ErrorData, CallToolResult, CancelledNotificationParam, Content, ErrorCode, ErrorData,
GetPromptRequestParams, GetPromptResult, Implementation, ListPromptsResult, LoggingLevel, GetPromptRequestParams, GetPromptResult, Implementation, ListPromptsResult, LoggingLevel,
LoggingMessageNotificationParam, PaginatedRequestParams, Prompt, PromptArgument, LoggingMessageNotificationParam, Meta, PaginatedRequestParams, Prompt, PromptArgument,
PromptMessage, PromptMessageRole, Role, ServerCapabilities, ServerInfo, PromptMessage, PromptMessageRole, Role, ServerCapabilities, ServerInfo,
}, },
schemars::JsonSchema, schemars::JsonSchema,
service::{NotificationContext, RequestContext}, service::{NotificationContext, RequestContext},
tool, tool_handler, tool_router, RoleServer, ServerHandler, tool, tool_handler, tool_router, RoleServer, ServerHandler,
}; };
/// Header name for passing working directory through MCP request metadata
const WORKING_DIR_HEADER: &str = "agent-working-dir";
/// Extract working directory from MCP request metadata
fn extract_working_dir_from_meta(meta: &Meta) -> Option<PathBuf> {
meta.0
.get(WORKING_DIR_HEADER)
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(PathBuf::from)
}
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashMap, collections::HashMap,
@@ -873,6 +886,8 @@ impl DeveloperServer {
let peer = context.peer; let peer = context.peer;
let request_id = context.id; let request_id = context.id;
let working_dir = extract_working_dir_from_meta(&context.meta);
// Validate the shell command // Validate the shell command
self.validate_shell_command(command)?; self.validate_shell_command(command)?;
@@ -886,7 +901,7 @@ impl DeveloperServer {
// Execute the command and capture output // Execute the command and capture output
let output_result = self let output_result = self
.execute_shell_command(command, &peer, cancellation_token.clone()) .execute_shell_command(command, &peer, cancellation_token.clone(), working_dir)
.await; .await;
// Clean up the process from tracking // Clean up the process from tracking
@@ -970,6 +985,7 @@ impl DeveloperServer {
command: &str, command: &str,
peer: &rmcp::service::Peer<RoleServer>, peer: &rmcp::service::Peer<RoleServer>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
working_dir: Option<PathBuf>,
) -> Result<String, ErrorData> { ) -> Result<String, ErrorData> {
let mut shell_config = ShellConfig::default(); let mut shell_config = ShellConfig::default();
let shell_name = std::path::Path::new(&shell_config.executable) let shell_name = std::path::Path::new(&shell_config.executable)
@@ -977,10 +993,6 @@ impl DeveloperServer {
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.unwrap_or("bash"); .unwrap_or("bash");
let working_dir = std::env::var("GOOSE_WORKING_DIR")
.ok()
.map(std::path::PathBuf::from);
if let Some(ref env_file) = self.bash_env_file { if let Some(ref env_file) = self.bash_env_file {
if shell_name == "bash" { if shell_name == "bash" {
shell_config.envs.push(( shell_config.envs.push((
+6 -1
View File
@@ -947,7 +947,12 @@ async fn call_tool(
let tool_result = agent let tool_result = agent
.extension_manager .extension_manager
.dispatch_tool_call(&payload.session_id, tool_call, CancellationToken::default()) .dispatch_tool_call(
&payload.session_id,
tool_call,
None,
CancellationToken::default(),
)
.await .await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
+1
View File
@@ -581,6 +581,7 @@ impl Agent {
.dispatch_tool_call( .dispatch_tool_call(
&session.id, &session.id,
tool_call.clone(), tool_call.clone(),
Some(session.working_dir.as_path()),
cancellation_token.unwrap_or_default(), cancellation_token.unwrap_or_default(),
) )
.await; .await;
@@ -531,6 +531,7 @@ impl McpClientTrait for AppsManagerClient {
session_id: &str, session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
_working_dir: Option<&str>,
_cancel_token: CancellationToken, _cancel_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
let result = match name { let result = match name {
@@ -297,6 +297,7 @@ impl McpClientTrait for ChatRecallClient {
session_id: &str, session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
_working_dir: Option<&str>,
_cancellation_token: CancellationToken, _cancellation_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
let content = match name { let content = match name {
@@ -262,7 +262,7 @@ fn create_tool_callback(
arguments: args.and_then(|v| v.as_object().cloned()), arguments: args.and_then(|v| v.as_object().cloned()),
}; };
match manager match manager
.dispatch_tool_call(&session_id, tool_call, CancellationToken::new()) .dispatch_tool_call(&session_id, tool_call, None, CancellationToken::new())
.await .await
{ {
Ok(dispatch_result) => match dispatch_result.result.await { Ok(dispatch_result) => match dispatch_result.result.await {
@@ -422,6 +422,7 @@ impl McpClientTrait for CodeExecutionClient {
session_id: &str, session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
_working_dir: Option<&str>,
_cancellation_token: CancellationToken, _cancellation_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
let result = match name { let result = match name {
+47 -18
View File
@@ -599,16 +599,6 @@ impl ExtensionManager {
.await?; .await?;
Box::new(client) Box::new(client)
} else { } else {
// Set GOOSE_WORKING_DIR in the current process for builtin extensions
// since they run in-process and read from std::env::var
if effective_working_dir.exists() && effective_working_dir.is_dir() {
std::env::set_var("GOOSE_WORKING_DIR", &effective_working_dir);
tracing::info!(
"Set GOOSE_WORKING_DIR for builtin extension: {:?}",
effective_working_dir
);
}
let (server_read, client_write) = tokio::io::duplex(65536); let (server_read, client_write) = tokio::io::duplex(65536);
let (client_read, server_write) = tokio::io::duplex(65536); let (client_read, server_write) = tokio::io::duplex(65536);
extension_fn(server_read, server_write); extension_fn(server_read, server_write);
@@ -1189,6 +1179,7 @@ impl ExtensionManager {
&self, &self,
session_id: &str, session_id: &str,
tool_call: CallToolRequestParams, tool_call: CallToolRequestParams,
working_dir: Option<&std::path::Path>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Result<ToolCallResult> { ) -> Result<ToolCallResult> {
// Some models strip the tool prefix, so auto-add it for known code_execution tools // Some models strip the tool prefix, so auto-add it for known code_execution tools
@@ -1248,16 +1239,24 @@ impl ExtensionManager {
let client = client.clone(); let client = client.clone();
let notifications_receiver = client.lock().await.subscribe().await; let notifications_receiver = client.lock().await.subscribe().await;
let session_id = session_id.to_string(); let session_id = session_id.to_string();
let working_dir_str = working_dir.map(|p| p.to_string_lossy().to_string());
let fut = async move { let fut = async move {
tracing::debug!( tracing::debug!(
"dispatch_tool_call fut: calling client.call_tool tool={} session_id={}", "dispatch_tool_call fut: calling client.call_tool tool={} session_id={} working_dir={:?}",
tool_name, tool_name,
session_id session_id,
working_dir_str
); );
let client_guard = client.lock().await; let client_guard = client.lock().await;
client_guard client_guard
.call_tool(&session_id, &tool_name, arguments, cancellation_token) .call_tool(
&session_id,
&tool_name,
arguments,
working_dir_str.as_deref(),
cancellation_token,
)
.await .await
.map_err(|e| match e { .map_err(|e| match e {
ServiceError::McpError(error_data) => error_data, ServiceError::McpError(error_data) => error_data,
@@ -1591,6 +1590,7 @@ mod tests {
_session_id: &str, _session_id: &str,
name: &str, name: &str,
_arguments: Option<JsonObject>, _arguments: Option<JsonObject>,
_working_dir: Option<&str>,
_cancellation_token: CancellationToken, _cancellation_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
match name { match name {
@@ -1727,7 +1727,12 @@ mod tests {
}; };
let result = extension_manager let result = extension_manager
.dispatch_tool_call("test-session-id", tool_call, CancellationToken::default()) .dispatch_tool_call(
"test-session-id",
tool_call,
None,
CancellationToken::default(),
)
.await; .await;
assert!(result.is_ok()); assert!(result.is_ok());
@@ -1739,7 +1744,12 @@ mod tests {
}; };
let result = extension_manager let result = extension_manager
.dispatch_tool_call("test-session-id", tool_call, CancellationToken::default()) .dispatch_tool_call(
"test-session-id",
tool_call,
None,
CancellationToken::default(),
)
.await; .await;
assert!(result.is_ok()); assert!(result.is_ok());
@@ -1752,7 +1762,12 @@ mod tests {
}; };
let result = extension_manager let result = extension_manager
.dispatch_tool_call("test-session-id", tool_call, CancellationToken::default()) .dispatch_tool_call(
"test-session-id",
tool_call,
None,
CancellationToken::default(),
)
.await; .await;
assert!(result.is_ok()); assert!(result.is_ok());
@@ -1765,7 +1780,12 @@ mod tests {
}; };
let result = extension_manager let result = extension_manager
.dispatch_tool_call("test-session-id", tool_call, CancellationToken::default()) .dispatch_tool_call(
"test-session-id",
tool_call,
None,
CancellationToken::default(),
)
.await; .await;
assert!(result.is_ok()); assert!(result.is_ok());
@@ -1777,7 +1797,12 @@ mod tests {
}; };
let result = extension_manager let result = extension_manager
.dispatch_tool_call("test-session-id", tool_call, CancellationToken::default()) .dispatch_tool_call(
"test-session-id",
tool_call,
None,
CancellationToken::default(),
)
.await; .await;
assert!(result.is_ok()); assert!(result.is_ok());
@@ -1793,6 +1818,7 @@ mod tests {
.dispatch_tool_call( .dispatch_tool_call(
"test-session-id", "test-session-id",
invalid_tool_call, invalid_tool_call,
None,
CancellationToken::default(), CancellationToken::default(),
) )
.await .await
@@ -1820,6 +1846,7 @@ mod tests {
.dispatch_tool_call( .dispatch_tool_call(
"test-session-id", "test-session-id",
invalid_tool_call, invalid_tool_call,
None,
CancellationToken::default(), CancellationToken::default(),
) )
.await; .await;
@@ -1922,6 +1949,7 @@ mod tests {
.dispatch_tool_call( .dispatch_tool_call(
"test-session-id", "test-session-id",
unavailable_tool_call, unavailable_tool_call,
None,
CancellationToken::default(), CancellationToken::default(),
) )
.await; .await;
@@ -1947,6 +1975,7 @@ mod tests {
.dispatch_tool_call( .dispatch_tool_call(
"test-session-id", "test-session-id",
available_tool_call, available_tool_call,
None,
CancellationToken::default(), CancellationToken::default(),
) )
.await; .await;
@@ -435,6 +435,7 @@ impl McpClientTrait for ExtensionManagerClient {
session_id: &str, session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
_working_dir: Option<&str>,
_cancellation_token: CancellationToken, _cancellation_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
let result = match name { let result = match name {
+54 -30
View File
@@ -1,6 +1,6 @@
use crate::action_required_manager::ActionRequiredManager; use crate::action_required_manager::ActionRequiredManager;
use crate::agents::types::SharedProvider; use crate::agents::types::SharedProvider;
use crate::session_context::SESSION_ID_HEADER; use crate::session_context::{SESSION_ID_HEADER, WORKING_DIR_HEADER};
use rmcp::model::{ use rmcp::model::{
Content, CreateElicitationRequestParams, CreateElicitationResult, ElicitationAction, ErrorCode, Content, CreateElicitationRequestParams, CreateElicitationResult, ElicitationAction, ErrorCode,
Extensions, JsonObject, Meta, Extensions, JsonObject, Meta,
@@ -51,6 +51,7 @@ pub trait McpClientTrait: Send + Sync {
session_id: &str, session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
working_dir: Option<&str>,
cancel_token: CancellationToken, cancel_token: CancellationToken,
) -> Result<CallToolResult, Error>; ) -> Result<CallToolResult, Error>;
@@ -105,7 +106,7 @@ pub trait McpClientTrait: Send + Sync {
pub struct GooseClient { pub struct GooseClient {
notification_handlers: Arc<Mutex<Vec<Sender<ServerNotification>>>>, notification_handlers: Arc<Mutex<Vec<Sender<ServerNotification>>>>,
provider: SharedProvider, provider: SharedProvider,
// Single-slot because calls are serialized per MCP client; see send_request_with_session. // Single-slot because calls are serialized per MCP client.
current_session_id: Arc<Mutex<Option<String>>>, current_session_id: Arc<Mutex<Option<String>>>,
} }
@@ -386,13 +387,14 @@ impl McpClient {
self.docker_container.as_deref() self.docker_container.as_deref()
} }
async fn send_request_with_session( async fn send_request_with_context(
&self, &self,
session_id: &str, session_id: &str,
working_dir: Option<&str>,
request: ClientRequest, request: ClientRequest,
cancel_token: CancellationToken, cancel_token: CancellationToken,
) -> Result<ServerResult, Error> { ) -> Result<ServerResult, Error> {
let request = inject_session_id_into_request(request, Some(session_id)); let request = inject_session_context_into_request(request, Some(session_id), working_dir);
// ExtensionManager serializes calls per MCP connection, so one current_session_id slot // ExtensionManager serializes calls per MCP connection, so one current_session_id slot
// is sufficient for mapping callbacks to the active request session. // is sufficient for mapping callbacks to the active request session.
let handle = { let handle = {
@@ -473,8 +475,9 @@ impl McpClientTrait for McpClient {
cancel_token: CancellationToken, cancel_token: CancellationToken,
) -> Result<ListResourcesResult, Error> { ) -> Result<ListResourcesResult, Error> {
let res = self let res = self
.send_request_with_session( .send_request_with_context(
session_id, session_id,
None,
ClientRequest::ListResourcesRequest(ListResourcesRequest { ClientRequest::ListResourcesRequest(ListResourcesRequest {
params: Some(PaginatedRequestParams { meta: None, cursor }), params: Some(PaginatedRequestParams { meta: None, cursor }),
method: Default::default(), method: Default::default(),
@@ -497,8 +500,9 @@ impl McpClientTrait for McpClient {
cancel_token: CancellationToken, cancel_token: CancellationToken,
) -> Result<ReadResourceResult, Error> { ) -> Result<ReadResourceResult, Error> {
let res = self let res = self
.send_request_with_session( .send_request_with_context(
session_id, session_id,
None,
ClientRequest::ReadResourceRequest(ReadResourceRequest { ClientRequest::ReadResourceRequest(ReadResourceRequest {
params: ReadResourceRequestParams { params: ReadResourceRequestParams {
meta: None, meta: None,
@@ -524,8 +528,9 @@ impl McpClientTrait for McpClient {
cancel_token: CancellationToken, cancel_token: CancellationToken,
) -> Result<ListToolsResult, Error> { ) -> Result<ListToolsResult, Error> {
let res = self let res = self
.send_request_with_session( .send_request_with_context(
session_id, session_id,
None,
ClientRequest::ListToolsRequest(ListToolsRequest { ClientRequest::ListToolsRequest(ListToolsRequest {
params: Some(PaginatedRequestParams { meta: None, cursor }), params: Some(PaginatedRequestParams { meta: None, cursor }),
method: Default::default(), method: Default::default(),
@@ -546,6 +551,7 @@ impl McpClientTrait for McpClient {
session_id: &str, session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
working_dir: Option<&str>,
cancel_token: CancellationToken, cancel_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
let request = ClientRequest::CallToolRequest(CallToolRequest { let request = ClientRequest::CallToolRequest(CallToolRequest {
@@ -560,7 +566,7 @@ impl McpClientTrait for McpClient {
}); });
let result = self let result = self
.send_request_with_session(session_id, request, cancel_token) .send_request_with_context(session_id, working_dir, request, cancel_token)
.await; .await;
match result? { match result? {
@@ -576,8 +582,9 @@ impl McpClientTrait for McpClient {
cancel_token: CancellationToken, cancel_token: CancellationToken,
) -> Result<ListPromptsResult, Error> { ) -> Result<ListPromptsResult, Error> {
let res = self let res = self
.send_request_with_session( .send_request_with_context(
session_id, session_id,
None,
ClientRequest::ListPromptsRequest(ListPromptsRequest { ClientRequest::ListPromptsRequest(ListPromptsRequest {
params: Some(PaginatedRequestParams { meta: None, cursor }), params: Some(PaginatedRequestParams { meta: None, cursor }),
method: Default::default(), method: Default::default(),
@@ -605,8 +612,9 @@ impl McpClientTrait for McpClient {
_ => None, _ => None,
}; };
let res = self let res = self
.send_request_with_session( .send_request_with_context(
session_id, session_id,
None,
ClientRequest::GetPromptRequest(GetPromptRequest { ClientRequest::GetPromptRequest(GetPromptRequest {
params: GetPromptRequestParams { params: GetPromptRequestParams {
meta: None, meta: None,
@@ -633,20 +641,24 @@ impl McpClientTrait for McpClient {
} }
} }
/// Injects the given session_id into Extensions._meta. /// Injects the given session_id and working_dir into Extensions._meta.
/// None (or empty) removes any existing session id. /// None (or empty) removes any existing values.
fn inject_session_id_into_extensions( fn inject_session_context_into_extensions(
mut extensions: Extensions, mut extensions: Extensions,
session_id: Option<&str>, session_id: Option<&str>,
working_dir: Option<&str>,
) -> Extensions { ) -> Extensions {
let session_id = session_id.filter(|id| !id.is_empty()); let session_id = session_id.filter(|id| !id.is_empty());
let working_dir = working_dir.filter(|dir| !dir.is_empty());
let mut meta_map = extensions let mut meta_map = extensions
.get::<Meta>() .get::<Meta>()
.map(|meta| meta.0.clone()) .map(|meta| meta.0.clone())
.unwrap_or_default(); .unwrap_or_default();
// JsonObject is case-sensitive, so we use retain for case-insensitive removal // JsonObject is case-sensitive, so we use retain for case-insensitive removal
meta_map.retain(|k, _| !k.eq_ignore_ascii_case(SESSION_ID_HEADER)); meta_map.retain(|k, _| {
!k.eq_ignore_ascii_case(SESSION_ID_HEADER) && !k.eq_ignore_ascii_case(WORKING_DIR_HEADER)
});
if let Some(session_id) = session_id { if let Some(session_id) = session_id {
meta_map.insert( meta_map.insert(
@@ -655,37 +667,51 @@ fn inject_session_id_into_extensions(
); );
} }
if let Some(working_dir) = working_dir {
meta_map.insert(
WORKING_DIR_HEADER.to_string(),
Value::String(working_dir.to_string()),
);
}
extensions.insert(Meta(meta_map)); extensions.insert(Meta(meta_map));
extensions extensions
} }
fn inject_session_id_into_request( fn inject_session_context_into_request(
request: ClientRequest, request: ClientRequest,
session_id: Option<&str>, session_id: Option<&str>,
working_dir: Option<&str>,
) -> ClientRequest { ) -> ClientRequest {
match request { match request {
ClientRequest::ListResourcesRequest(mut req) => { ClientRequest::ListResourcesRequest(mut req) => {
req.extensions = inject_session_id_into_extensions(req.extensions, session_id); req.extensions =
inject_session_context_into_extensions(req.extensions, session_id, working_dir);
ClientRequest::ListResourcesRequest(req) ClientRequest::ListResourcesRequest(req)
} }
ClientRequest::ReadResourceRequest(mut req) => { ClientRequest::ReadResourceRequest(mut req) => {
req.extensions = inject_session_id_into_extensions(req.extensions, session_id); req.extensions =
inject_session_context_into_extensions(req.extensions, session_id, working_dir);
ClientRequest::ReadResourceRequest(req) ClientRequest::ReadResourceRequest(req)
} }
ClientRequest::ListToolsRequest(mut req) => { ClientRequest::ListToolsRequest(mut req) => {
req.extensions = inject_session_id_into_extensions(req.extensions, session_id); req.extensions =
inject_session_context_into_extensions(req.extensions, session_id, working_dir);
ClientRequest::ListToolsRequest(req) ClientRequest::ListToolsRequest(req)
} }
ClientRequest::CallToolRequest(mut req) => { ClientRequest::CallToolRequest(mut req) => {
req.extensions = inject_session_id_into_extensions(req.extensions, session_id); req.extensions =
inject_session_context_into_extensions(req.extensions, session_id, working_dir);
ClientRequest::CallToolRequest(req) ClientRequest::CallToolRequest(req)
} }
ClientRequest::ListPromptsRequest(mut req) => { ClientRequest::ListPromptsRequest(mut req) => {
req.extensions = inject_session_id_into_extensions(req.extensions, session_id); req.extensions =
inject_session_context_into_extensions(req.extensions, session_id, working_dir);
ClientRequest::ListPromptsRequest(req) ClientRequest::ListPromptsRequest(req)
} }
ClientRequest::GetPromptRequest(mut req) => { ClientRequest::GetPromptRequest(mut req) => {
req.extensions = inject_session_id_into_extensions(req.extensions, session_id); req.extensions =
inject_session_context_into_extensions(req.extensions, session_id, working_dir);
ClientRequest::GetPromptRequest(req) ClientRequest::GetPromptRequest(req)
} }
other => other, other => other,
@@ -814,7 +840,8 @@ mod tests {
*slot = Some(session_id.to_string()); *slot = Some(session_id.to_string());
} }
let extensions = inject_session_id_into_extensions(Extensions::new(), ext_session); let extensions =
inject_session_context_into_extensions(Extensions::new(), ext_session, None);
let resolved = client.resolve_session_id(&extensions).await; let resolved = client.resolve_session_id(&extensions).await;
@@ -830,8 +857,6 @@ mod tests {
#[test_case(list_prompts_request; "list_prompts")] #[test_case(list_prompts_request; "list_prompts")]
#[test_case(get_prompt_request; "get_prompt")] #[test_case(get_prompt_request; "get_prompt")]
fn test_request_injects_session(request_builder: fn(Extensions) -> ClientRequest) { fn test_request_injects_session(request_builder: fn(Extensions) -> ClientRequest) {
use serde_json::json;
let session_id = "test-session-id"; let session_id = "test-session-id";
let mut extensions = Extensions::new(); let mut extensions = Extensions::new();
extensions.insert( extensions.insert(
@@ -843,7 +868,7 @@ mod tests {
); );
let request = request_builder(extensions); let request = request_builder(extensions);
let request = inject_session_id_into_request(request, Some(session_id)); let request = inject_session_context_into_request(request, Some(session_id), None);
let extensions = request_extensions(&request).expect("request should have extensions"); let extensions = request_extensions(&request).expect("request should have extensions");
let meta = extensions let meta = extensions
.get::<Meta>() .get::<Meta>()
@@ -861,10 +886,9 @@ mod tests {
#[test] #[test]
fn test_session_id_in_mcp_meta() { fn test_session_id_in_mcp_meta() {
use serde_json::json;
let session_id = "test-session-789"; let session_id = "test-session-789";
let extensions = inject_session_id_into_extensions(Default::default(), Some(session_id)); let extensions =
inject_session_context_into_extensions(Default::default(), Some(session_id), None);
let mcp_meta = extensions.get::<Meta>().unwrap(); let mcp_meta = extensions.get::<Meta>().unwrap();
assert_eq!( assert_eq!(
@@ -904,7 +928,7 @@ mod tests {
expected_meta: serde_json::Value, expected_meta: serde_json::Value,
) { ) {
use rmcp::model::Extensions; use rmcp::model::Extensions;
use serde_json::{from_value, json}; use serde_json::from_value;
let mut extensions = Extensions::new(); let mut extensions = Extensions::new();
extensions.insert( extensions.insert(
@@ -916,7 +940,7 @@ mod tests {
.unwrap(), .unwrap(),
); );
let extensions = inject_session_id_into_extensions(extensions, session_id); let extensions = inject_session_context_into_extensions(extensions, session_id, None);
let mcp_meta = extensions.get::<Meta>().unwrap(); let mcp_meta = extensions.get::<Meta>().unwrap();
assert_eq!(&mcp_meta.0, expected_meta.as_object().unwrap()); assert_eq!(&mcp_meta.0, expected_meta.as_object().unwrap());
@@ -306,6 +306,7 @@ impl McpClientTrait for SkillsClient {
_session_id: &str, _session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
_working_dir: Option<&str>,
_cancellation_token: CancellationToken, _cancellation_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
let content = match name { let content = match name {
@@ -174,6 +174,7 @@ impl McpClientTrait for TodoClient {
session_id: &str, session_id: &str,
name: &str, name: &str,
arguments: Option<JsonObject>, arguments: Option<JsonObject>,
_working_dir: Option<&str>,
_cancellation_token: CancellationToken, _cancellation_token: CancellationToken,
) -> Result<CallToolResult, Error> { ) -> Result<CallToolResult, Error> {
let content = match name { let content = match name {
+1
View File
@@ -1,6 +1,7 @@
use tokio::task_local; use tokio::task_local;
pub const SESSION_ID_HEADER: &str = "agent-session-id"; pub const SESSION_ID_HEADER: &str = "agent-session-id";
pub const WORKING_DIR_HEADER: &str = "agent-working-dir";
task_local! { task_local! {
pub static SESSION_ID: Option<String>; pub static SESSION_ID: Option<String>;
+6 -1
View File
@@ -267,7 +267,12 @@ async fn test_replayed_session(
arguments: tool_call.arguments, arguments: tool_call.arguments,
}; };
let result = extension_manager let result = extension_manager
.dispatch_tool_call("test-session-id", tool_call, CancellationToken::default()) .dispatch_tool_call(
"test-session-id",
tool_call,
None,
CancellationToken::default(),
)
.await; .await;
let tool_result = result?; let tool_result = result?;
@@ -3,7 +3,6 @@ import { FolderDot } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
import { updateWorkingDir } from '../../api'; import { updateWorkingDir } from '../../api';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { setCurrentWorkingDir } from '../../utils/workingDir';
interface DirSwitcherProps { interface DirSwitcherProps {
className: string; className: string;
@@ -43,7 +42,6 @@ export const DirSwitcher: React.FC<DirSwitcherProps> = ({
const newDir = result.filePaths[0]; const newDir = result.filePaths[0];
window.electron.addRecentDir(newDir); window.electron.addRecentDir(newDir);
setCurrentWorkingDir(newDir);
if (sessionId) { if (sessionId) {
onWorkingDirChange?.(newDir); onWorkingDirChange?.(newDir);
+2 -9
View File
@@ -1,11 +1,4 @@
// Track the current working dir for this window (updated when user changes it)
let currentWorkingDir: string | null = null;
export const setCurrentWorkingDir = (dir: string): void => {
currentWorkingDir = dir;
};
export const getInitialWorkingDir = (): string => { export const getInitialWorkingDir = (): string => {
// Use the current dir if set, otherwise fall back to initial config // Fall back to initial config from app startup
return currentWorkingDir ?? (window.appConfig?.get('GOOSE_WORKING_DIR') as string) ?? ''; return (window.appConfig?.get('GOOSE_WORKING_DIR') as string) ?? '';
}; };