pub struct ProviderSessionId(/* private fields */);Expand description
A provider’s own name for its conversation — an external correlation id, never the episode’s identity.
It is how a provider’s hook finds the episode it opened: the hook passes
--provider-session <x>, the open record lives at state/sessions-open/<key of x>.yaml,
and an end event for x closes that episode and no other. That is the whole of its job.
It is not the session identity, for three reasons that are each a measured incident
rather than a worry. An episode opened by hand has no provider session at all and is
keyed hand, so the identity would be missing for the one episode a person opens.
Two providers may name their sessions in the same shape, so the value is unique only
within a provider. And the value changes under the worker: on 2026-09-09 a session in
this repository was re-established, came back under a different peer identity, and was
invisible to eight others for three hours because a durable thing had been keyed by a
value that is not.
The stored key is reduced to one path segment so that nothing a provider sends can name a file outside the store.
use majordomus_cli::session::ProviderSessionId;
let p = ProviderSessionId::new("01Bv2gJsfAoYrXnpfC9uJ4uv");
assert_eq!(p.store_key(), "01Bv2gJsfAoYrXnpfC9uJ4uv");
// anything that could leave the store becomes an inert segment
assert_eq!(ProviderSessionId::new("../../etc/passwd").store_key(), ".._.._etc_passwd");
assert_eq!(ProviderSessionId::new("a/b").store_key(), "a_b");
// and a segment made only of dots is not a directory: `.` and `..` survive an
// allow-list that permits a dot, so the segment is what is checked, not the bytes
assert_eq!(ProviderSessionId::new("..").store_key(), "_..");
assert_eq!(ProviderSessionId::new(".").store_key(), "_.");
// an episode nobody named is the hand-opened one, and there is at most one
assert_eq!(ProviderSessionId::hand().store_key(), "hand");
assert!(ProviderSessionId::hand().is_hand());
assert!(ProviderSessionId::new(" ").is_hand(), "a blank name names nobody");Implementations§
Source§impl ProviderSessionId
impl ProviderSessionId
Sourcepub fn new(value: impl Into<String>) -> Self
pub fn new(value: impl Into<String>) -> Self
The provider’s own string, as sent. Nothing about its shape is assumed: it is a provider’s private identifier, and this type’s job is to stop it being used as an episode identity rather than to validate it.
use majordomus_cli::session::ProviderSessionId;
assert_eq!(ProviderSessionId::new("01Bv2gJsf").as_str(), "01Bv2gJsf");Sourcepub fn hand() -> Self
pub fn hand() -> Self
The episode nobody named: a provider that sends no session identity is indistinguishable from a person at a terminal, and inventing a distinction there would multiply episodes nobody can close.
use majordomus_cli::session::ProviderSessionId;
assert!(ProviderSessionId::hand().is_hand());
assert_eq!(ProviderSessionId::hand().store_key(), "hand");Sourcepub fn is_hand(&self) -> bool
pub fn is_hand(&self) -> bool
Is this the hand-opened episode’s key? True for an empty or blank value. A provider that sends whitespace has named nobody just as surely as one that sends nothing, so both are the hand-opened episode.
use majordomus_cli::session::ProviderSessionId;
assert!(ProviderSessionId::new(" \t ").is_hand());
assert!(!ProviderSessionId::new("01Bv2gJsf").is_hand());Sourcepub fn as_str(&self) -> &str
pub fn as_str(&self) -> &str
The value as sent, which is what a record carries and what a hook passes back. Not
the file name: ProviderSessionId::store_key is that, and the two differ
exactly when the provider sent something a path segment may not contain.
use majordomus_cli::session::ProviderSessionId;
let p = ProviderSessionId::new("a/b");
assert_eq!(p.as_str(), "a/b");
assert_ne!(p.as_str(), p.store_key());Sourcepub fn store_key(&self) -> String
pub fn store_key(&self) -> String
The value reduced to exactly one path segment, for the file the open record lives
in. Every byte outside A-Za-z0-9._- becomes _; the blank value becomes hand.
A value made only of dots gets a _ in front of it. . and .. survive an
allow-list that permits a dot — they are made of nothing else — and both name a
directory rather than a file in it. The test that found this is in this module: an
allow-list is a claim about bytes, and the claim that has to hold is about the
segment.
use majordomus_cli::session::ProviderSessionId;
assert_eq!(ProviderSessionId::new("01Bv2gJsf").store_key(), "01Bv2gJsf");
assert!(!ProviderSessionId::new("../etc/passwd").store_key().contains('/'));Trait Implementations§
Source§impl Clone for ProviderSessionId
impl Clone for ProviderSessionId
Source§fn clone(&self) -> ProviderSessionId
fn clone(&self) -> ProviderSessionId
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for ProviderSessionId
impl Debug for ProviderSessionId
Source§impl<'de> Deserialize<'de> for ProviderSessionId
impl<'de> Deserialize<'de> for ProviderSessionId
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for ProviderSessionId
Source§impl Hash for ProviderSessionId
impl Hash for ProviderSessionId
Source§impl JsonSchema for ProviderSessionId
impl JsonSchema for ProviderSessionId
Source§fn schema_id() -> Cow<'static, str>
fn schema_id() -> Cow<'static, str>
Source§fn json_schema(generator: &mut SchemaGenerator) -> Schema
fn json_schema(generator: &mut SchemaGenerator) -> Schema
Source§fn inline_schema() -> bool
fn inline_schema() -> bool
$ref keyword. Read moreSource§impl Ord for ProviderSessionId
impl Ord for ProviderSessionId
Source§fn cmp(&self, other: &ProviderSessionId) -> Ordering
fn cmp(&self, other: &ProviderSessionId) -> Ordering
1.21.0 (const: unstable) · Source§fn max(self, other: Self) -> Selfwhere
Self: Sized,
fn max(self, other: Self) -> Selfwhere
Self: Sized,
Source§impl PartialEq for ProviderSessionId
impl PartialEq for ProviderSessionId
Source§fn eq(&self, other: &ProviderSessionId) -> bool
fn eq(&self, other: &ProviderSessionId) -> bool
self and other values to be equal, and is used by ==.Source§impl PartialOrd for ProviderSessionId
impl PartialOrd for ProviderSessionId
Source§impl Serialize for ProviderSessionId
impl Serialize for ProviderSessionId
impl StructuralPartialEq for ProviderSessionId
Auto Trait Implementations§
impl Freeze for ProviderSessionId
impl RefUnwindSafe for ProviderSessionId
impl Send for ProviderSessionId
impl Sync for ProviderSessionId
impl Unpin for ProviderSessionId
impl UnsafeUnpin for ProviderSessionId
impl UnwindSafe for ProviderSessionId
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<Q, K> Comparable<K> for Q
impl<Q, K> Comparable<K> for Q
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.