Files
huskies/server/src/http/mcp/session.rs
T

52 lines
1.9 KiB
Rust

//! Task-local session identifier for the requesting MCP client.
//!
//! Threaded ambiently through tool dispatch (same pattern as
//! [`super::progress::EMITTER`]) so a deeply-nested handler — currently only
//! `tool_prompt_permission` — can scope decisions per requesting agent
//! without adding a parameter to `dispatch_tool_call` and every one of its
//! ~40 match arms.
//!
//! The HTTP MCP handler installs the scope before dispatching a `tools/call`
//! request, populated from the `X-Huskies-Session` header that per-story
//! worktrees embed in their `.mcp.json` (see `worktree::write_mcp_json`).
//! Callers with no header (the main interactive chat CLI, API-based runtimes
//! that invoke `dispatch_tool_call` directly) fall back to a fixed
//! `"default"` key — there is only ever one such session per server process,
//! so no cross-story leakage results from sharing that bucket.
/// Session key used when no `X-Huskies-Session` header was present.
pub const DEFAULT_SESSION: &str = "default";
tokio::task_local! {
/// Set by the MCP HTTP handler before dispatching a `tools/call` request.
/// Unset in tests and in non-HTTP dispatch paths, where [`current`] falls
/// back to [`DEFAULT_SESSION`].
pub static SESSION_ID: String;
}
/// Return the current request's session id, or [`DEFAULT_SESSION`] if no
/// scope is installed.
pub fn current() -> String {
SESSION_ID
.try_with(Clone::clone)
.unwrap_or_else(|_| DEFAULT_SESSION.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn current_falls_back_to_default_without_scope() {
assert_eq!(current(), DEFAULT_SESSION);
}
#[tokio::test]
async fn current_reads_installed_scope() {
let value = SESSION_ID
.scope("1218".to_string(), async { current() })
.await;
assert_eq!(value, "1218");
}
}