Skip to main content

hara_native/runtime/
session_model.rs

1use std::fmt;
2
3/// Stable identity for one process-local Hara session.
4#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
5pub struct SessionId(String);
6
7impl SessionId {
8    pub fn parse(value: &str) -> Result<Self, String> {
9        if value.is_empty()
10            || !value
11                .bytes()
12                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.' | b'-'))
13        {
14            return Err("INVALID_SESSION_NAME".into());
15        }
16        Ok(Self(value.into()))
17    }
18
19    pub fn as_str(&self) -> &str {
20        &self.0
21    }
22}
23
24impl AsRef<str> for SessionId {
25    fn as_ref(&self) -> &str {
26        self.as_str()
27    }
28}
29
30impl fmt::Display for SessionId {
31    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32        formatter.write_str(self.as_str())
33    }
34}
35
36/// Identity of one logical filesystem resource managed by the local Kernel.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct SessionMountId(u64);
39
40impl SessionMountId {
41    pub const fn new(value: u64) -> Self {
42        assert!(value > 0, "filesystem mount identifiers must be positive");
43        Self(value)
44    }
45
46    pub const fn get(self) -> u64 {
47        self.0
48    }
49}
50
51impl fmt::Display for SessionMountId {
52    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53        self.0.fmt(formatter)
54    }
55}
56
57/// Observable lifecycle of one Session.
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum SessionState {
60    New,
61    Active,
62    Closed,
63}
64
65impl SessionState {
66    pub const fn as_str(self) -> &'static str {
67        match self {
68            Self::New => "new",
69            Self::Active => "active",
70            Self::Closed => "closed",
71        }
72    }
73}
74
75impl fmt::Display for SessionState {
76    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77        formatter.write_str(self.as_str())
78    }
79}
80
81/// Authority inherited from an embedding host by one in-process evaluator.
82///
83/// This policy describes direct host authority only. A filesystem mounted
84/// explicitly on a Session is a separate scoped delegation and does not turn
85/// that Session into a host-filesystem session. In-process namespace and
86/// Runtime separation remain logical isolation, not a security boundary.
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct SessionAuthorityPolicy {
89    pub host_filesystem: bool,
90    pub host_network: bool,
91    pub host_process: bool,
92    pub reflection: bool,
93    pub packages: bool,
94    pub project: bool,
95}
96
97impl SessionAuthorityPolicy {
98    pub const ZERO: Self = Self {
99        host_filesystem: false,
100        host_network: false,
101        host_process: false,
102        reflection: false,
103        packages: false,
104        project: false,
105    };
106
107    pub const fn profile(self) -> &'static str {
108        if !self.host_filesystem
109            && !self.host_network
110            && !self.host_process
111            && !self.reflection
112            && !self.packages
113            && !self.project
114        {
115            "zero"
116        } else {
117            "explicit"
118        }
119    }
120}
121
122/// Immutable construction contract for one ordinary in-process Session.
123///
124/// The Session owns the Runtime created from this specification. Live mounts
125/// and provider implementations remain Kernel-owned resources and are attached
126/// separately.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct SessionSpec {
129    pub id: SessionId,
130    pub authority: SessionAuthorityPolicy,
131}
132
133impl SessionSpec {
134    pub fn new(id: SessionId, authority: SessionAuthorityPolicy) -> Self {
135        Self { id, authority }
136    }
137
138    pub fn zero_authority(name: &str) -> Result<Self, String> {
139        Ok(Self::new(
140            SessionId::parse(name)?,
141            SessionAuthorityPolicy::ZERO,
142        ))
143    }
144}
145
146/// Immutable status projection for one Session.
147#[derive(Clone, Debug, PartialEq, Eq)]
148pub struct SessionStatus {
149    pub name: SessionId,
150    pub namespace: String,
151    pub state: SessionState,
152    pub filesystem: Option<SessionMountId>,
153    pub authority: SessionAuthorityPolicy,
154}
155
156/// Compatibility name retained while the Session/Runtime model is tightened.
157pub type SessionMetadata = SessionStatus;
158
159#[cfg(test)]
160mod session_model_tests {
161    use super::*;
162
163    #[test]
164    fn session_identity_and_mount_identifiers_are_distinct_types() {
165        let session = SessionId::parse("workspace.alpha").unwrap();
166        let mount = SessionMountId::new(7);
167        assert_eq!(session.as_str(), "workspace.alpha");
168        assert_eq!(mount.get(), 7);
169        assert_eq!(mount.to_string(), "7");
170        assert!(SessionId::parse("bad/name").is_err());
171    }
172
173    #[test]
174    #[should_panic(expected = "filesystem mount identifiers must be positive")]
175    fn mount_identifiers_reject_zero() {
176        SessionMountId::new(0);
177    }
178
179    #[test]
180    fn zero_authority_spec_is_explicit_and_immutable() {
181        let spec = SessionSpec::zero_authority("child").unwrap();
182        assert_eq!(spec.id.as_str(), "child");
183        assert_eq!(spec.authority, SessionAuthorityPolicy::ZERO);
184        assert_eq!(spec.authority.profile(), "zero");
185    }
186
187    #[test]
188    fn session_lifecycle_states_are_explicit() {
189        assert_eq!(SessionState::New.as_str(), "new");
190        assert_eq!(SessionState::Active.as_str(), "active");
191        assert_eq!(SessionState::Closed.as_str(), "closed");
192    }
193}