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//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use super::{CallSource, RequestContext};
13use http::{HeaderMap, HeaderValue};
14use std::str::FromStr;
15use systemprompt_identifiers::{
16    Actor, AgentName, AiToolCallId, ClientId, ContextId, JwtToken, SessionId, TaskId, TraceId,
17    UserId, headers,
18};
19use systemprompt_traits::{
20    ContextPropagation, ContextPropagationError, ContextPropagationResult, InjectContextHeaders,
21};
22
23fn insert_header(headers: &mut HeaderMap, name: &'static str, value: &str) {
24    match HeaderValue::from_str(value) {
25        Ok(val) => {
26            headers.insert(name, val);
27        },
28        Err(e) => {
29            tracing::warn!(
30                header = %name,
31                value = %value,
32                error = %e,
33                "Invalid header value - header not inserted"
34            );
35        },
36    }
37}
38
39fn insert_header_if_present(headers: &mut HeaderMap, name: &'static str, value: Option<&str>) {
40    if let Some(v) = value {
41        insert_header(headers, name, v);
42    }
43}
44
45impl InjectContextHeaders for RequestContext {
46    fn inject_headers(&self, hdrs: &mut HeaderMap) {
47        insert_header(hdrs, headers::SESSION_ID, self.request.session_id.as_str());
48        insert_header(hdrs, headers::TRACE_ID, self.execution.trace_id.as_str());
49        insert_header(hdrs, headers::USER_ID, self.auth.actor.user_id.as_str());
50        insert_header(hdrs, headers::USER_TYPE, self.auth.user_type.as_str());
51        insert_header(
52            hdrs,
53            headers::AGENT_NAME,
54            self.execution.agent_name.as_str(),
55        );
56
57        insert_header(
58            hdrs,
59            headers::CONTEXT_ID,
60            self.execution.context_id.as_str(),
61        );
62
63        insert_header_if_present(
64            hdrs,
65            headers::TASK_ID,
66            self.execution.task_id.as_ref().map(TaskId::as_str),
67        );
68        insert_header_if_present(
69            hdrs,
70            headers::AI_TOOL_CALL_ID,
71            self.execution.ai_tool_call_id.as_ref().map(AsRef::as_ref),
72        );
73        insert_header_if_present(
74            hdrs,
75            headers::CALL_SOURCE,
76            self.execution.call_source.as_ref().map(CallSource::as_str),
77        );
78        insert_header_if_present(
79            hdrs,
80            headers::CLIENT_ID,
81            self.request.client_id.as_ref().map(ClientId::as_str),
82        );
83
84        match &self.auth.auth_token {
85            Some(auth_token) => {
86                let auth_value = format!("Bearer {}", auth_token.as_str());
87                insert_header(hdrs, headers::AUTHORIZATION, &auth_value);
88                tracing::trace!(user_id = %self.auth.actor.user_id, "Injected Authorization header for proxy");
89            },
90            None => {
91                tracing::trace!(user_id = %self.auth.actor.user_id, "No auth_token to inject - Authorization header not added");
92            },
93        }
94
95        if let Some(user) = &self.user {
96            insert_header(hdrs, headers::PROXY_VERIFIED, "true");
97            let perms = crate::auth::permissions_to_string(&user.permissions);
98            insert_header(hdrs, headers::USER_PERMISSIONS, &perms);
99            let roles = crate::auth::roles_to_string(&user.roles);
100            insert_header(hdrs, headers::USER_ROLES, &roles);
101        }
102    }
103}
104
105fn header_str<'h>(hdrs: &'h HeaderMap, name: &'static str) -> Option<&'h str> {
106    hdrs.get(name).and_then(|v| v.to_str().ok())
107}
108
109fn required_header<'h>(
110    hdrs: &'h HeaderMap,
111    name: &'static str,
112) -> ContextPropagationResult<&'h str> {
113    header_str(hdrs, name).ok_or_else(|| ContextPropagationError::MissingHeader(name.to_owned()))
114}
115
116fn invalid_header(
117    name: &'static str,
118    source: impl Into<systemprompt_traits::BoxedSource>,
119) -> ContextPropagationError {
120    ContextPropagationError::InvalidHeader {
121        name: name.to_owned(),
122        source: source.into(),
123    }
124}
125
126fn apply_optional_execution_fields(
127    mut ctx: RequestContext,
128    hdrs: &HeaderMap,
129) -> ContextPropagationResult<RequestContext> {
130    if let Some(s) = header_str(hdrs, headers::TASK_ID) {
131        ctx = ctx.with_task_id(TaskId::new(s.to_owned()));
132    }
133    if let Some(s) = header_str(hdrs, headers::AI_TOOL_CALL_ID) {
134        ctx = ctx.with_ai_tool_call_id(AiToolCallId::new(s.to_owned()));
135    }
136    if let Some(s) = header_str(hdrs, headers::CALL_SOURCE) {
137        let cs = CallSource::from_str(s).map_err(|e| invalid_header(headers::CALL_SOURCE, e))?;
138        ctx = ctx.with_call_source(cs);
139    }
140    if let Some(s) = header_str(hdrs, headers::CLIENT_ID) {
141        ctx = ctx.with_client_id(ClientId::new(s.to_owned()));
142    }
143    let auth_token = header_str(hdrs, headers::AUTHORIZATION)
144        .and_then(|s| s.strip_prefix("Bearer "))
145        .filter(|token| !token.is_empty());
146    if let Some(token) = auth_token {
147        ctx = ctx.with_auth_token(JwtToken::new(token));
148    }
149    Ok(ctx)
150}
151
152fn apply_proxy_verified_user(
153    mut ctx: RequestContext,
154    hdrs: &HeaderMap,
155    user_id: &UserId,
156) -> ContextPropagationResult<RequestContext> {
157    let proxy_verified = header_str(hdrs, headers::PROXY_VERIFIED).is_some_and(|v| v == "true");
158    if !proxy_verified {
159        return Ok(ctx);
160    }
161
162    // Why: a verified mesh request without a permissions header is a request
163    // the proxy did not decorate (absence); a header that fails to parse is a
164    // corrupted trust claim and must reject rather than downgrade to anonymous.
165    let Some(raw_permissions) = header_str(hdrs, headers::USER_PERMISSIONS) else {
166        return Ok(ctx);
167    };
168    let permissions = crate::auth::parse_permissions(raw_permissions)
169        .map_err(|e| invalid_header(headers::USER_PERMISSIONS, e))?;
170
171    let roles = header_str(hdrs, headers::USER_ROLES)
172        .map(crate::auth::parse_roles)
173        .unwrap_or_default();
174    let user = crate::auth::AuthenticatedUser::new_with_roles(
175        user_id.clone(),
176        String::new(),
177        String::new(),
178        permissions,
179        roles,
180    );
181    ctx = ctx.with_user(user);
182    Ok(ctx)
183}
184
185impl ContextPropagation for RequestContext {
186    fn from_headers(hdrs: &HeaderMap) -> ContextPropagationResult<Self> {
187        let session_id = required_header(hdrs, headers::SESSION_ID)?;
188        let trace_id = required_header(hdrs, headers::TRACE_ID)?;
189        let user_id = UserId::try_new(required_header(hdrs, headers::USER_ID)?)
190            .map_err(|e| invalid_header(headers::USER_ID, e))?;
191        let agent_name = required_header(hdrs, headers::AGENT_NAME)?;
192
193        let session_id = SessionId::new(session_id.to_owned());
194        let context_id = match header_str(hdrs, headers::CONTEXT_ID).filter(|s| !s.is_empty()) {
195            Some(s) => ContextId::try_new(s).map_err(|e| invalid_header(headers::CONTEXT_ID, e))?,
196            None => ContextId::derived_from_session(&session_id),
197        };
198
199        let agent_name = AgentName::try_new(agent_name.to_owned())
200            .map_err(|e| invalid_header(headers::AGENT_NAME, e))?;
201
202        let ctx = Self::new(
203            session_id,
204            TraceId::new(trace_id.to_owned()),
205            context_id,
206            agent_name,
207            Actor::user(user_id.clone()),
208        );
209
210        let ctx = apply_optional_execution_fields(ctx, hdrs)?;
211        apply_proxy_verified_user(ctx, hdrs, &user_id)
212    }
213
214    fn to_headers(&self) -> HeaderMap {
215        let mut headers = HeaderMap::new();
216        self.inject_headers(&mut headers);
217        headers
218    }
219}