//! 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"); } }