Skip to main content

lean_ctx_protocol/
runtime_session.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Bounded replay state for one pre-admitted, capability-scoped local session.
3//! The host supplies trusted scope and a fresh session/key, never scope learned
4//! from an incoming request. This mechanism does not decide policy or trust peers.
5
6use std::fmt;
7
8use crate::{
9    EngineInvocationV1, EngineOperationV1, EnginePolicyDecisionV1, ProtocolReference,
10    ResolvedLocalEngineIdentityV1,
11    runtime_exchange::RuntimeRequestV1,
12    runtime_frame::{RuntimeFrameDirection, RuntimeFrameError, decode_runtime_frame},
13    validate_bounded_opaque_identifier,
14};
15
16/// No Clone, Debug or serialization: neither the borrowed key nor session state
17/// may be copied into another receiver to reset its replay counter.
18pub struct RuntimeRequestSession<'key> {
19    key: &'key [u8; 32],
20    session_id: String,
21    engine: ResolvedLocalEngineIdentityV1,
22    operation: EngineOperationV1,
23    policy_ref: ProtocolReference,
24    next_sequence: Option<u64>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum RuntimeSessionError {
29    InvalidScope,
30    Frame(RuntimeFrameError),
31    InvalidRequest,
32    ScopeMismatch,
33    SequenceMismatch,
34    SequenceExhausted,
35}
36
37impl fmt::Display for RuntimeSessionError {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        formatter.write_str(match self {
40            Self::InvalidScope => "invalid runtime session scope",
41            Self::Frame(_) => "invalid authenticated runtime frame",
42            Self::InvalidRequest => "invalid runtime request",
43            Self::ScopeMismatch => "runtime request outside session scope",
44            Self::SequenceMismatch => "runtime request sequence mismatch",
45            Self::SequenceExhausted => "runtime session sequence exhausted",
46        })
47    }
48}
49
50impl std::error::Error for RuntimeSessionError {}
51
52impl<'key> RuntimeRequestSession<'key> {
53    /// Bind locally trusted admission, not a peer-supplied assertion of admission.
54    /// The owner provisions and protects the key, serializes access to this
55    /// receiver, and retires it on reconnect/revocation. Never reuse a session key.
56    pub fn new(
57        key: &'key [u8; 32],
58        session_id: String,
59        admitted: &EngineInvocationV1,
60    ) -> Result<Self, RuntimeSessionError> {
61        validate_bounded_opaque_identifier(&session_id, "runtime session_id")
62            .and_then(|()| admitted.validate())
63            .map_err(|_| RuntimeSessionError::InvalidScope)?;
64        if admitted.policy_admission.decision != EnginePolicyDecisionV1::Admitted {
65            return Err(RuntimeSessionError::InvalidScope);
66        }
67        Ok(Self {
68            key,
69            session_id,
70            engine: admitted.engine.clone(),
71            operation: admitted.operation.clone(),
72            policy_ref: admitted.policy_admission.policy_ref.clone(),
73            next_sequence: Some(1),
74        })
75    }
76
77    /// Authenticate before JSON parsing; admit exact scope and consecutive sequence.
78    /// Invalid input never advances state. A successful request is consumed even
79    /// if later execution fails, so it cannot be replayed to repeat side effects.
80    /// Callers still enforce a monotonic I/O deadline and live policy revocation.
81    pub fn accept(
82        &mut self,
83        frame: &[u8],
84        now_unix_ms: u64,
85    ) -> Result<RuntimeRequestV1, RuntimeSessionError> {
86        let sequence = self
87            .next_sequence
88            .ok_or(RuntimeSessionError::SequenceExhausted)?;
89        let payload = decode_runtime_frame(self.key, RuntimeFrameDirection::Request, frame)
90            .map_err(RuntimeSessionError::Frame)?;
91        let request = RuntimeRequestV1::from_bytes(&payload, now_unix_ms)
92            .map_err(|_| RuntimeSessionError::InvalidRequest)?;
93        if request.session_id != self.session_id
94            || request.invocation.engine != self.engine
95            || request.invocation.operation != self.operation
96            || request.invocation.policy_admission.policy_ref != self.policy_ref
97        {
98            return Err(RuntimeSessionError::ScopeMismatch);
99        }
100        if request.sequence != sequence {
101            return Err(RuntimeSessionError::SequenceMismatch);
102        }
103        self.next_sequence = sequence.checked_add(1);
104        Ok(request)
105    }
106}
107
108#[cfg(test)]
109mod tests;