Skip to main content

conversation_api/execution/
context.rs

1//! Invocation identity and tracing context.
2
3use serde::{Deserialize, Serialize};
4
5use crate::execution::{
6    ClientId, ConversationSurface, OperationId, RunId, ScopeId, TenantId, ThreadId, UserId,
7};
8
9/// Ownership boundary used for authorization and persistence partitioning.
10#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
11#[serde(deny_unknown_fields)]
12pub struct Scope {
13    /// Optional tenant for multi-tenant deployments.
14    pub tenant_id: Option<TenantId>,
15    /// Application scope that owns Agent state and application capabilities.
16    pub scope_id: ScopeId,
17}
18
19/// Authenticated actor that initiated an Agent run.
20#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct Actor {
23    /// Original authority; a Space Client never becomes a user through a relay.
24    pub principal: crate::ClientPrincipal,
25    /// Enrollment responsible for this run. Required for Space Clients.
26    pub credential_id: Option<String>,
27    /// Optional originating application client used for routed responses.
28    pub client_id: Option<ClientId>,
29}
30
31impl Actor {
32    #[must_use]
33    pub fn user(user_id: impl Into<UserId>, client_id: Option<ClientId>) -> Self {
34        Self {
35            principal: crate::ClientPrincipal::User {
36                user_id: user_id.into().as_str().to_owned(),
37            },
38            credential_id: None,
39            client_id,
40        }
41    }
42
43    pub fn user_id(&self) -> Option<&str> {
44        self.principal.user_id()
45    }
46
47    /// Validate binding after transport authentication. This does not authorize tools.
48    pub fn valid_for(&self, scope: &Scope, surface: &ConversationSurface) -> bool {
49        if self
50            .client_id
51            .as_ref()
52            .is_some_and(|id| !valid_id(id.as_str()))
53            || self
54                .credential_id
55                .as_deref()
56                .is_some_and(|id| !valid_id(id))
57        {
58            return false;
59        }
60        match &self.principal {
61            crate::ClientPrincipal::User { user_id } => {
62                valid_id(user_id)
63                    && !surface.is_space_clients()
64                    && surface.user_id().is_none_or(|owner| owner == user_id)
65            }
66            crate::ClientPrincipal::Space { .. } => {
67                self.client_id.is_some()
68                    && self.credential_id.is_some()
69                    && self.principal.validate_for(
70                        scope.tenant_id.as_ref().map_or("", |id| id.as_str()),
71                        scope.scope_id.as_str(),
72                    )
73                    && surface.is_space_clients()
74            }
75        }
76    }
77}
78
79fn valid_id(value: &str) -> bool {
80    !value.is_empty() && value.trim() == value && !value.chars().any(char::is_control)
81}
82
83/// Context that follows one Agent invocation across all ports.
84#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct InvocationContext {
87    /// Authorization and persistence scope.
88    pub scope: Scope,
89    /// Authenticated actor; never accepted from model-generated arguments.
90    pub actor: Actor,
91    /// Trusted, canonical conversation space. Runtime uses it only as thread identity.
92    pub surface_id: ConversationSurface,
93    /// Durable conversation thread.
94    pub thread_id: ThreadId,
95    /// Current run identifier.
96    pub run_id: RunId,
97    /// Idempotency identifier for the initiating operation.
98    pub operation_id: OperationId,
99    /// Optional absolute Unix deadline in milliseconds.
100    pub deadline_unix_ms: Option<u64>,
101    /// Optional W3C traceparent propagated by the deployment.
102    pub traceparent: Option<String>,
103}