Skip to main content

a2a_rs/port/
request_context.rs

1//! What a transport knows about an inbound call, beyond its payload.
2
3use crate::port::authenticator::AuthPrincipal;
4
5/// Who is calling, and which conversation the call belongs to.
6///
7/// Built by the transport adapter that accepted the request and passed down
8/// through `TaskService` to
9/// [`AsyncMessageHandler`](crate::port::AsyncMessageHandler). One value rather
10/// than a widening parameter list: the session id, the principal and (later) the
11/// tenant are all facts about *this* request, and a handler that wants one
12/// usually wants the others.
13///
14/// Not to be confused with [`CallContext`](crate::port::CallContext), which is
15/// interceptor metadata about the call being dispatched (method name, side of
16/// the wire) and says nothing about who made it.
17///
18/// # The principal
19///
20/// [`principal`](Self::principal) is `None` when the agent serves anonymous
21/// callers — no authenticator is configured, so there is nobody to name. It is
22/// what [`AsyncConversationStore`](crate::port::AsyncConversationStore) compares
23/// against a context's recorded owner, which is why the transport has to carry
24/// it rather than the handler guessing: a handler that cannot tell two callers
25/// apart hands the second one the first one's conversation.
26#[derive(Debug, Clone, Default)]
27pub struct RequestContext {
28    /// The context id the caller supplied, if any. Empty on a first turn that
29    /// lets the agent pick one.
30    session_id: Option<String>,
31    /// The authenticated principal, or `None` on an agent that does not
32    /// authenticate.
33    principal: Option<AuthPrincipal>,
34}
35
36impl RequestContext {
37    /// A call from nobody in particular: no session, no principal.
38    ///
39    /// The right context for an internal caller that is not serving a request —
40    /// a bridge invoking a handler directly, or a test.
41    pub fn anonymous() -> Self {
42        Self::default()
43    }
44
45    /// Name the conversation this call belongs to.
46    ///
47    /// An empty string is the same as no session at all, which is what a wire
48    /// message with an unset `context_id` decodes to.
49    #[must_use]
50    pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
51        let session_id = session_id.into();
52        self.session_id = (!session_id.is_empty()).then_some(session_id);
53        self
54    }
55
56    /// Attach the principal the transport authenticated.
57    #[must_use]
58    pub fn with_principal(mut self, principal: impl Into<Option<AuthPrincipal>>) -> Self {
59        self.principal = principal.into();
60        self
61    }
62
63    /// The context id the caller supplied.
64    pub fn session_id(&self) -> Option<&str> {
65        self.session_id.as_deref()
66    }
67
68    /// The authenticated principal, with whatever claims the authenticator
69    /// attached.
70    pub fn principal(&self) -> Option<&AuthPrincipal> {
71        self.principal.as_ref()
72    }
73
74    /// The authenticated principal's id — the identity a conversation is owned
75    /// by.
76    pub fn caller(&self) -> Option<&str> {
77        self.principal.as_ref().map(|p| p.id.as_str())
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn an_anonymous_call_names_nobody() {
87        let ctx = RequestContext::anonymous();
88        assert_eq!(ctx.session_id(), None);
89        assert_eq!(ctx.caller(), None);
90    }
91
92    #[test]
93    fn an_empty_session_id_is_no_session() {
94        // What an unset wire `context_id` decodes to, so the transports do not
95        // each have to remember to filter it.
96        let ctx = RequestContext::anonymous().with_session("");
97        assert_eq!(ctx.session_id(), None);
98    }
99
100    #[test]
101    fn the_caller_is_the_principals_id() {
102        let ctx = RequestContext::anonymous()
103            .with_session("ctx-1")
104            .with_principal(AuthPrincipal::new("alice".to_string(), "jwt".to_string()));
105
106        assert_eq!(ctx.session_id(), Some("ctx-1"));
107        assert_eq!(ctx.caller(), Some("alice"));
108        assert_eq!(ctx.principal().map(|p| p.scheme.as_str()), Some("jwt"));
109    }
110}