lean_ctx_protocol/
runtime_session.rs1use std::fmt;
7
8use crate::{
9 EngineInvocationV1, EngineOperationV1, EnginePolicyDecisionV1, ProtocolReference,
10 ResolvedLocalEngineIdentityV1,
11 runtime_exchange::{RuntimeExchangeProfile, RuntimeRequestV1},
12 runtime_frame::{RuntimeFrameDirection, RuntimeFrameError, decode_runtime_frame_in},
13 validate_bounded_opaque_identifier,
14};
15
16pub struct RuntimeRequestSession<'key> {
19 key: &'key [u8; 32],
20 session_id: String,
21 engine: ResolvedLocalEngineIdentityV1,
22 operation: EngineOperationV1,
23 policy_ref: ProtocolReference,
24 profile: RuntimeExchangeProfile,
26 next_sequence: Option<u64>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum RuntimeSessionError {
31 InvalidScope,
32 Frame(RuntimeFrameError),
33 InvalidRequest,
34 ScopeMismatch,
35 SequenceMismatch,
36 SequenceExhausted,
37}
38
39impl fmt::Display for RuntimeSessionError {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 formatter.write_str(match self {
42 Self::InvalidScope => "invalid runtime session scope",
43 Self::Frame(_) => "invalid authenticated runtime frame",
44 Self::InvalidRequest => "invalid runtime request",
45 Self::ScopeMismatch => "runtime request outside session scope",
46 Self::SequenceMismatch => "runtime request sequence mismatch",
47 Self::SequenceExhausted => "runtime session sequence exhausted",
48 })
49 }
50}
51
52impl std::error::Error for RuntimeSessionError {}
53
54impl<'key> RuntimeRequestSession<'key> {
55 pub fn new(
59 key: &'key [u8; 32],
60 session_id: String,
61 admitted: &EngineInvocationV1,
62 ) -> Result<Self, RuntimeSessionError> {
63 Self::new_in(key, session_id, admitted, RuntimeExchangeProfile::ORDINARY)
64 }
65
66 pub fn new_in(
71 key: &'key [u8; 32],
72 session_id: String,
73 admitted: &EngineInvocationV1,
74 profile: RuntimeExchangeProfile,
75 ) -> Result<Self, RuntimeSessionError> {
76 validate_bounded_opaque_identifier(&session_id, "runtime session_id")
77 .and_then(|()| admitted.validate())
78 .map_err(|_| RuntimeSessionError::InvalidScope)?;
79 if admitted.policy_admission.decision != EnginePolicyDecisionV1::Admitted {
80 return Err(RuntimeSessionError::InvalidScope);
81 }
82 Ok(Self {
83 key,
84 session_id,
85 engine: admitted.engine.clone(),
86 operation: admitted.operation.clone(),
87 policy_ref: admitted.policy_admission.policy_ref.clone(),
88 profile,
89 next_sequence: Some(1),
90 })
91 }
92
93 pub fn accept(
98 &mut self,
99 frame: &[u8],
100 now_unix_ms: u64,
101 ) -> Result<RuntimeRequestV1, RuntimeSessionError> {
102 let sequence = self
103 .next_sequence
104 .ok_or(RuntimeSessionError::SequenceExhausted)?;
105 let payload = decode_runtime_frame_in(
106 self.key,
107 RuntimeFrameDirection::Request,
108 frame,
109 self.profile,
110 )
111 .map_err(RuntimeSessionError::Frame)?;
112 let request = RuntimeRequestV1::from_bytes_in(&payload, now_unix_ms, self.profile)
113 .map_err(|_| RuntimeSessionError::InvalidRequest)?;
114 if request.session_id != self.session_id
115 || request.invocation.engine != self.engine
116 || request.invocation.operation != self.operation
117 || request.invocation.policy_admission.policy_ref != self.policy_ref
118 {
119 return Err(RuntimeSessionError::ScopeMismatch);
120 }
121 if request.sequence != sequence {
122 return Err(RuntimeSessionError::SequenceMismatch);
123 }
124 self.next_sequence = sequence.checked_add(1);
125 Ok(request)
126 }
127}
128
129#[cfg(test)]
130mod tests;