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, SessionId, TaskId, TraceId, UserId,
17    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        let auth_token = self.auth.auth_token.as_str();
85        if auth_token.is_empty() {
86            tracing::trace!(user_id = %self.auth.actor.user_id, "No auth_token to inject - Authorization header not added");
87        } else {
88            let auth_value = format!("Bearer {}", auth_token);
89            insert_header(hdrs, headers::AUTHORIZATION, &auth_value);
90            tracing::trace!(user_id = %self.auth.actor.user_id, "Injected Authorization header for proxy");
91        }
92
93        if let Some(user) = &self.user {
94            insert_header(hdrs, headers::PROXY_VERIFIED, "true");
95            let perms = crate::auth::permissions_to_string(&user.permissions);
96            insert_header(hdrs, headers::USER_PERMISSIONS, &perms);
97        }
98    }
99}
100
101fn header_str<'h>(hdrs: &'h HeaderMap, name: &'static str) -> Option<&'h str> {
102    hdrs.get(name).and_then(|v| v.to_str().ok())
103}
104
105fn required_header<'h>(
106    hdrs: &'h HeaderMap,
107    name: &'static str,
108) -> ContextPropagationResult<&'h str> {
109    header_str(hdrs, name).ok_or_else(|| ContextPropagationError::MissingHeader(name.to_owned()))
110}
111
112fn apply_optional_execution_fields(mut ctx: RequestContext, hdrs: &HeaderMap) -> RequestContext {
113    if let Some(s) = header_str(hdrs, headers::TASK_ID) {
114        ctx = ctx.with_task_id(TaskId::new(s.to_owned()));
115    }
116    if let Some(s) = header_str(hdrs, headers::AI_TOOL_CALL_ID) {
117        ctx = ctx.with_ai_tool_call_id(AiToolCallId::new(s.to_owned()));
118    }
119    let call_source =
120        header_str(hdrs, headers::CALL_SOURCE).and_then(|s| CallSource::from_str(s).ok());
121    if let Some(cs) = call_source {
122        ctx = ctx.with_call_source(cs);
123    }
124    if let Some(s) = header_str(hdrs, headers::CLIENT_ID) {
125        ctx = ctx.with_client_id(ClientId::new(s.to_owned()));
126    }
127    let auth_token =
128        header_str(hdrs, headers::AUTHORIZATION).and_then(|s| s.strip_prefix("Bearer "));
129    if let Some(token) = auth_token {
130        ctx = ctx.with_auth_token(token.to_owned());
131    }
132    ctx
133}
134
135fn apply_proxy_verified_user(
136    mut ctx: RequestContext,
137    hdrs: &HeaderMap,
138    user_id: &str,
139) -> ContextPropagationResult<RequestContext> {
140    let proxy_verified = header_str(hdrs, headers::PROXY_VERIFIED).is_some_and(|v| v == "true");
141    if !proxy_verified {
142        return Ok(ctx);
143    }
144
145    let Some(permissions) = header_str(hdrs, headers::USER_PERMISSIONS)
146        .and_then(|s| crate::auth::parse_permissions(s).ok())
147    else {
148        return Ok(ctx);
149    };
150
151    let user_id_uuid =
152        user_id
153            .parse::<uuid::Uuid>()
154            .map_err(|e| ContextPropagationError::InvalidHeader {
155                name: headers::USER_ID.to_owned(),
156                message: format!("invalid UUID: {e}"),
157            })?;
158    let user = crate::auth::AuthenticatedUser::new(
159        user_id_uuid,
160        String::new(),
161        String::new(),
162        permissions,
163    );
164    ctx = ctx.with_user(user);
165    Ok(ctx)
166}
167
168impl ContextPropagation for RequestContext {
169    fn from_headers(hdrs: &HeaderMap) -> ContextPropagationResult<Self> {
170        let session_id = required_header(hdrs, headers::SESSION_ID)?;
171        let trace_id = required_header(hdrs, headers::TRACE_ID)?;
172        let user_id = required_header(hdrs, headers::USER_ID)?;
173        let agent_name = required_header(hdrs, headers::AGENT_NAME)?;
174
175        let context_id = header_str(hdrs, headers::CONTEXT_ID)
176            .filter(|s| !s.is_empty())
177            .and_then(|s| ContextId::try_new(s).ok())
178            .unwrap_or_else(ContextId::generate);
179
180        let ctx = Self::new(
181            SessionId::new(session_id.to_owned()),
182            TraceId::new(trace_id.to_owned()),
183            context_id,
184            AgentName::new(agent_name.to_owned()),
185        )
186        .with_actor(Actor::user(UserId::new(user_id.to_owned())));
187
188        let ctx = apply_optional_execution_fields(ctx, hdrs);
189        apply_proxy_verified_user(ctx, hdrs, user_id)
190    }
191
192    fn to_headers(&self) -> HeaderMap {
193        let mut headers = HeaderMap::new();
194        self.inject_headers(&mut headers);
195        headers
196    }
197}