Skip to main content

systemprompt_models/execution/context/
propagation.rs

1//! HTTP-header propagation of [`RequestContext`] across service hops.
2//!
3//! Implements [`InjectContextHeaders`] and [`ContextPropagation`] for
4//! [`RequestContext`]: serializing identity, trace, and execution fields into
5//! outbound headers and reconstructing them inbound. The proxy-verified path
6//! reconstructs the [`AuthenticatedUser`](crate::auth::AuthenticatedUser) only
7//! when an upstream proxy has asserted trust via the `proxy-verified` header.
8
9use super::{CallSource, RequestContext};
10use http::{HeaderMap, HeaderValue};
11use std::str::FromStr;
12use systemprompt_identifiers::{
13    Actor, AgentName, AiToolCallId, ClientId, ContextId, SessionId, TaskId, TraceId, UserId,
14    headers,
15};
16use systemprompt_traits::{
17    ContextPropagation, ContextPropagationError, ContextPropagationResult, InjectContextHeaders,
18};
19
20fn insert_header(headers: &mut HeaderMap, name: &'static str, value: &str) {
21    match HeaderValue::from_str(value) {
22        Ok(val) => {
23            headers.insert(name, val);
24        },
25        Err(e) => {
26            tracing::warn!(
27                header = %name,
28                value = %value,
29                error = %e,
30                "Invalid header value - header not inserted"
31            );
32        },
33    }
34}
35
36fn insert_header_if_present(headers: &mut HeaderMap, name: &'static str, value: Option<&str>) {
37    if let Some(v) = value {
38        insert_header(headers, name, v);
39    }
40}
41
42impl InjectContextHeaders for RequestContext {
43    fn inject_headers(&self, hdrs: &mut HeaderMap) {
44        insert_header(hdrs, headers::SESSION_ID, self.request.session_id.as_str());
45        insert_header(hdrs, headers::TRACE_ID, self.execution.trace_id.as_str());
46        insert_header(hdrs, headers::USER_ID, self.auth.actor.user_id.as_str());
47        insert_header(hdrs, headers::USER_TYPE, self.auth.user_type.as_str());
48        insert_header(
49            hdrs,
50            headers::AGENT_NAME,
51            self.execution.agent_name.as_str(),
52        );
53
54        insert_header(
55            hdrs,
56            headers::CONTEXT_ID,
57            self.execution.context_id.as_str(),
58        );
59
60        insert_header_if_present(
61            hdrs,
62            headers::TASK_ID,
63            self.execution.task_id.as_ref().map(TaskId::as_str),
64        );
65        insert_header_if_present(
66            hdrs,
67            headers::AI_TOOL_CALL_ID,
68            self.execution.ai_tool_call_id.as_ref().map(AsRef::as_ref),
69        );
70        insert_header_if_present(
71            hdrs,
72            headers::CALL_SOURCE,
73            self.execution.call_source.as_ref().map(CallSource::as_str),
74        );
75        insert_header_if_present(
76            hdrs,
77            headers::CLIENT_ID,
78            self.request.client_id.as_ref().map(ClientId::as_str),
79        );
80
81        let auth_token = self.auth.auth_token.as_str();
82        if auth_token.is_empty() {
83            tracing::trace!(user_id = %self.auth.actor.user_id, "No auth_token to inject - Authorization header not added");
84        } else {
85            let auth_value = format!("Bearer {}", auth_token);
86            insert_header(hdrs, headers::AUTHORIZATION, &auth_value);
87            tracing::trace!(user_id = %self.auth.actor.user_id, "Injected Authorization header for proxy");
88        }
89
90        if let Some(user) = &self.user {
91            insert_header(hdrs, headers::PROXY_VERIFIED, "true");
92            let perms = crate::auth::permissions_to_string(&user.permissions);
93            insert_header(hdrs, headers::USER_PERMISSIONS, &perms);
94        }
95    }
96}
97
98impl ContextPropagation for RequestContext {
99    fn from_headers(hdrs: &HeaderMap) -> ContextPropagationResult<Self> {
100        let session_id = hdrs
101            .get(headers::SESSION_ID)
102            .and_then(|v| v.to_str().ok())
103            .ok_or_else(|| {
104                ContextPropagationError::MissingHeader(headers::SESSION_ID.to_owned())
105            })?;
106
107        let trace_id = hdrs
108            .get(headers::TRACE_ID)
109            .and_then(|v| v.to_str().ok())
110            .ok_or_else(|| ContextPropagationError::MissingHeader(headers::TRACE_ID.to_owned()))?;
111
112        let user_id = hdrs
113            .get(headers::USER_ID)
114            .and_then(|v| v.to_str().ok())
115            .ok_or_else(|| ContextPropagationError::MissingHeader(headers::USER_ID.to_owned()))?;
116
117        let context_id = hdrs
118            .get(headers::CONTEXT_ID)
119            .and_then(|v| v.to_str().ok())
120            .filter(|s| !s.is_empty())
121            .and_then(|s| ContextId::try_new(s).ok())
122            .unwrap_or_else(ContextId::generate);
123
124        let agent_name = hdrs
125            .get(headers::AGENT_NAME)
126            .and_then(|v| v.to_str().ok())
127            .ok_or_else(|| {
128                ContextPropagationError::MissingHeader(headers::AGENT_NAME.to_owned())
129            })?;
130
131        let task_id = hdrs
132            .get(headers::TASK_ID)
133            .and_then(|v| v.to_str().ok())
134            .map(|s| TaskId::new(s.to_owned()));
135
136        let ai_tool_call_id = hdrs
137            .get(headers::AI_TOOL_CALL_ID)
138            .and_then(|v| v.to_str().ok())
139            .map(|s| AiToolCallId::new(s.to_owned()));
140
141        let call_source = hdrs
142            .get(headers::CALL_SOURCE)
143            .and_then(|v| v.to_str().ok())
144            .and_then(|s| CallSource::from_str(s).ok());
145
146        let client_id = hdrs
147            .get(headers::CLIENT_ID)
148            .and_then(|v| v.to_str().ok())
149            .map(|s| ClientId::new(s.to_owned()));
150
151        let auth_token = hdrs
152            .get(headers::AUTHORIZATION)
153            .and_then(|v| v.to_str().ok())
154            .and_then(|s| s.strip_prefix("Bearer "))
155            .map(str::to_owned);
156
157        let mut ctx = Self::new(
158            SessionId::new(session_id.to_owned()),
159            TraceId::new(trace_id.to_owned()),
160            context_id,
161            AgentName::new(agent_name.to_owned()),
162        )
163        .with_actor(Actor::user(UserId::new(user_id.to_owned())));
164
165        if let Some(tid) = task_id {
166            ctx = ctx.with_task_id(tid);
167        }
168
169        if let Some(ai_id) = ai_tool_call_id {
170            ctx = ctx.with_ai_tool_call_id(ai_id);
171        }
172
173        if let Some(cs) = call_source {
174            ctx = ctx.with_call_source(cs);
175        }
176
177        if let Some(cid) = client_id {
178            ctx = ctx.with_client_id(cid);
179        }
180
181        if let Some(token) = auth_token {
182            ctx = ctx.with_auth_token(token);
183        }
184
185        let proxy_verified = hdrs
186            .get(headers::PROXY_VERIFIED)
187            .and_then(|v| v.to_str().ok())
188            .is_some_and(|v| v == "true");
189
190        if proxy_verified {
191            if let Some(permissions) = hdrs
192                .get(headers::USER_PERMISSIONS)
193                .and_then(|v| v.to_str().ok())
194                .and_then(|s| crate::auth::parse_permissions(s).ok())
195            {
196                let user_id_uuid = user_id.parse::<uuid::Uuid>().map_err(|e| {
197                    ContextPropagationError::InvalidHeader {
198                        name: headers::USER_ID.to_owned(),
199                        message: format!("invalid UUID: {e}"),
200                    }
201                })?;
202                let user = crate::auth::AuthenticatedUser::new(
203                    user_id_uuid,
204                    String::new(),
205                    String::new(),
206                    permissions,
207                );
208                ctx = ctx.with_user(user);
209            }
210        }
211
212        Ok(ctx)
213    }
214
215    fn to_headers(&self) -> HeaderMap {
216        let mut headers = HeaderMap::new();
217        self.inject_headers(&mut headers);
218        headers
219    }
220}