Skip to main content

connector_client/
identity.rs

1//! Endpoint identity verification; local files are discovery hints, never authority.
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct AppIdentity {
9    pub app_instance_id: String,
10    pub app_id: String,
11    pub pid: u32,
12    pub started_at: u64,
13    #[serde(default)]
14    pub workspace_id: Option<String>,
15    #[serde(default)]
16    pub workspace_path: Option<PathBuf>,
17    pub inspection_protocol_version: u64,
18    #[serde(default)]
19    pub workflow_protocol_version: Option<u64>,
20}
21
22impl AppIdentity {
23    pub fn parse(value: Value) -> Result<Self, String> {
24        let identity: Self = serde_json::from_value(value)
25            .map_err(|e| format!("identity_unavailable: invalid endpoint identity: {e}"))?;
26        if identity.app_instance_id.is_empty() || identity.app_id.is_empty() || identity.pid == 0 {
27            return Err("identity_unavailable: incomplete endpoint identity".into());
28        }
29        Ok(identity)
30    }
31}
32
33/// Canonical filesystem containment uses path segments, never textual prefixes.
34pub fn workspace_matches(cwd: &Path, workspace: &Path) -> bool {
35    match (cwd.canonicalize(), workspace.canonicalize()) {
36        (Ok(cwd), Ok(workspace)) => cwd.starts_with(workspace),
37        _ => false,
38    }
39}
40
41pub fn instance_from_handle(handle: &str) -> Option<&str> {
42    handle
43        .split_once(":picker:")
44        .or_else(|| handle.split_once(":capture:"))
45        .or_else(|| handle.split_once(":artifact:"))
46        .map(|(instance, _)| instance)
47        .filter(|s| !s.is_empty())
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    #[test]
54    fn canonical_workspace_boundaries_and_symlinks() {
55        let root =
56            std::env::temp_dir().join(format!("connector-identity-{}", uuid::Uuid::new_v4()));
57        let app = root.join("app");
58        let apple = root.join("apple");
59        std::fs::create_dir_all(app.join("child")).unwrap();
60        std::fs::create_dir_all(&apple).unwrap();
61        assert!(workspace_matches(&app.join("child"), &app));
62        assert!(!workspace_matches(&apple, &app));
63        assert!(!workspace_matches(&app, &root.join("missing")));
64        #[cfg(unix)]
65        {
66            let alias = root.join("alias");
67            std::os::unix::fs::symlink(&app, &alias).unwrap();
68            assert!(workspace_matches(&alias.join("child"), &app));
69        }
70        std::fs::remove_dir_all(root).unwrap();
71    }
72}