use trusty_mpm::client::DaemonClient;
pub(crate) const DENY_BY_DEFAULT_ENV: &str = "TRUSTY_MPM_PM_DENY_BY_DEFAULT";
const MANAGED_SESSION_ID_ENV: &str = "TM_MANAGED_SESSION_ID";
pub(crate) const PERSONA_DENY_REASON: &str = "PM session persona deployment explicitly failed \
(TRUSTY_MPM_PM_DENY_BY_DEFAULT strict mode). Delegated edit blocked until the managed \
session's persona is repaired (see `tm doctor`) or the session is restarted.";
pub(crate) fn is_truthy(raw: Option<&str>) -> bool {
raw.is_some_and(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
}
pub(crate) fn deny_by_default_enabled() -> bool {
is_truthy(std::env::var(DENY_BY_DEFAULT_ENV).ok().as_deref())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PersonaStatus {
NotManaged,
Confirmed,
ExplicitlyFailed,
Ambiguous,
}
impl PersonaStatus {
pub(crate) fn should_deny(self) -> bool {
matches!(self, Self::ExplicitlyFailed)
}
}
fn classify_persona_status(session_id_present: bool, state: Option<&str>) -> PersonaStatus {
if !session_id_present {
return PersonaStatus::NotManaged;
}
match state {
Some("errored") => PersonaStatus::ExplicitlyFailed,
Some(_) => PersonaStatus::Confirmed,
None => PersonaStatus::Ambiguous,
}
}
async fn lookup_session_state(url: &str, session_id: &str) -> Option<String> {
let client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_millis(500))
.timeout(std::time::Duration::from_secs(2))
.build()
.ok()?;
let daemon = DaemonClient::with_client(client, url);
daemon
.get_managed_session(session_id)
.await
.ok()
.map(|s| s.state)
}
pub(crate) async fn persona_status_for_session(
raw_session_id: Option<&str>,
url: &str,
) -> PersonaStatus {
let Some(session_id) = raw_session_id.map(str::trim).filter(|s| !s.is_empty()) else {
return classify_persona_status(false, None);
};
let state = lookup_session_state(url, session_id).await;
classify_persona_status(true, state.as_deref())
}
pub(crate) async fn persona_status(url: &str) -> PersonaStatus {
let raw = std::env::var(MANAGED_SESSION_ID_ENV).ok();
persona_status_for_session(raw.as_deref(), url).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_truthy_accepts_known_tokens() {
for t in ["1", "true", "TRUE", "Yes", "on", " on "] {
assert!(is_truthy(Some(t)), "{t:?} should be truthy");
}
}
#[test]
fn is_truthy_rejects_unknown_or_absent() {
for f in [
None,
Some("0"),
Some("false"),
Some("no"),
Some("off"),
Some(""),
] {
assert!(!is_truthy(f), "{f:?} should not be truthy");
}
}
#[test]
fn classify_persona_status_not_managed_without_session_id() {
assert_eq!(
classify_persona_status(false, None),
PersonaStatus::NotManaged
);
}
#[test]
fn classify_persona_status_confirmed_for_non_errored_state() {
for state in ["active", "provisioning", "stopped"] {
assert_eq!(
classify_persona_status(true, Some(state)),
PersonaStatus::Confirmed,
"state {state:?} must classify as Confirmed"
);
}
}
#[test]
fn classify_persona_status_explicitly_failed_for_errored_state() {
assert_eq!(
classify_persona_status(true, Some("errored")),
PersonaStatus::ExplicitlyFailed
);
}
#[test]
fn classify_persona_status_ambiguous_when_state_unresolved() {
assert_eq!(
classify_persona_status(true, None),
PersonaStatus::Ambiguous
);
}
#[test]
fn persona_status_should_deny_only_for_explicitly_failed() {
assert!(PersonaStatus::ExplicitlyFailed.should_deny());
for allowed in [
PersonaStatus::NotManaged,
PersonaStatus::Confirmed,
PersonaStatus::Ambiguous,
] {
assert!(
!allowed.should_deny(),
"{allowed:?} must never deny (the #2172 permissive default)"
);
}
}
#[tokio::test]
async fn persona_status_for_session_not_managed_when_session_id_absent() {
let status = persona_status_for_session(None, "http://127.0.0.1:1").await;
assert_eq!(status, PersonaStatus::NotManaged);
}
#[tokio::test]
async fn persona_status_for_session_ambiguous_when_daemon_unreachable() {
let status = persona_status_for_session(
Some("11111111-1111-1111-1111-111111111111"),
"http://127.0.0.1:1",
)
.await;
assert_eq!(status, PersonaStatus::Ambiguous);
}
async fn spawn_mock_daemon(state: &str) -> String {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("addr");
let body = format!(
r#"{{"id":"11111111-1111-1111-1111-111111111111","name":"tmpm-test","state":"{state}"}}"#
);
tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.expect("accept");
let mut buf = [0u8; 1024];
let _ = sock.read(&mut buf).await;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let _ = sock.write_all(resp.as_bytes()).await;
let _ = sock.shutdown().await;
});
format!("http://{addr}")
}
#[tokio::test]
async fn persona_status_for_session_confirmed_for_active_state() {
let url = spawn_mock_daemon("active").await;
let status =
persona_status_for_session(Some("11111111-1111-1111-1111-111111111111"), &url).await;
assert_eq!(status, PersonaStatus::Confirmed);
}
#[tokio::test]
async fn persona_status_for_session_explicitly_failed_for_errored_state() {
let url = spawn_mock_daemon("errored").await;
let status =
persona_status_for_session(Some("11111111-1111-1111-1111-111111111111"), &url).await;
assert_eq!(status, PersonaStatus::ExplicitlyFailed);
}
}