Skip to main content

kanade_shared/ipc/
handshake.rs

1//! `system.handshake` types (SPEC §2.12.6).
2//!
3//! Every KLP connection's first request. Negotiates the protocol
4//! version and the agent's optional features, and returns the
5//! OS-derived session info (user SID + console session id + pc_id)
6//! the client uses to label its UI and audit-log entries.
7//!
8//! Until handshake completes, the agent rejects every other method
9//! with [`super::error::ErrorKind::InvalidRequest`] (SPEC §2.12.6
10//! "Handshake 未完了の状態で他 method を呼ぶと -32600 InvalidRequest").
11
12use serde::{Deserialize, Serialize};
13
14/// The single protocol version KLP v1 ships with. Listed in
15/// [`HandshakeParams::protocol`] by the client; the agent picks the
16/// highest mutually-supported version into
17/// [`HandshakeResult::protocol`]. If no overlap, the agent returns
18/// [`super::error::ErrorKind::StaleProtocol`].
19pub const PROTOCOL_V1: u32 = 1;
20
21/// `system.handshake` request params.
22#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
23pub struct HandshakeParams {
24    /// Client identifier (e.g. `"kanade-client"`). Free-form, used
25    /// for the audit log + tracing spans. Agent does not gate on
26    /// this — alternate clients (test harnesses, ops CLI) are fine.
27    pub client: String,
28    /// Client binary version (semver). Surfaced into the audit log.
29    pub client_version: String,
30    /// All protocol versions the client can speak. The agent picks
31    /// the highest mutually-supported version into
32    /// [`HandshakeResult::protocol`]. Length must be ≥ 1 — an empty
33    /// array returns `InvalidParams`.
34    pub protocol: Vec<u32>,
35    /// Optional features the client wants to use. Agent answers
36    /// with its own [`HandshakeResult::features`] set; the
37    /// intersection is what's actually live for this connection.
38    /// Defaults to `[]` for clients that need only the core methods.
39    #[serde(default)]
40    pub features: Vec<String>,
41}
42
43/// `system.handshake` response result.
44#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
45pub struct HandshakeResult {
46    /// Protocol version the agent picked from
47    /// [`HandshakeParams::protocol`].
48    pub protocol: u32,
49    /// Agent binary version. Drives the client's "Agent version
50    /// mismatch — restart?" toast.
51    pub agent_version: String,
52    /// Features the agent itself supports. Per SPEC §2.12.6 this
53    /// MAY be a superset of what the client asked for — the client
54    /// is expected to enable only the intersection.
55    pub features: Vec<String>,
56    /// Session identity (SPEC §2.12.4) — agent reads this from the
57    /// OS at connect time, not from the client payload, so the
58    /// values here are authoritative.
59    pub session: HandshakeSession,
60}
61
62/// Session info derived from the OS at connect time, surfaced back
63/// to the client so the UI can label "logged in as
64/// `DOMAIN\\alice`" without a second round-trip.
65#[derive(Serialize, Deserialize, schemars::JsonSchema, Debug, Clone)]
66pub struct HandshakeSession {
67    /// `DOMAIN\\username` (Windows) or `username` (Linux/macOS).
68    /// Derived from the OS token, not the payload.
69    pub user: String,
70    /// Console / RDP session id (Windows). On Linux/macOS this is
71    /// the UID (which serves the same "which interactive shell is
72    /// the caller in" role).
73    pub session_id: u32,
74    /// The PC's `pc_id` (matches the agent's `pc_id` everywhere
75    /// else — `Heartbeat`, `ExecResult`, audit log, …). Carried in
76    /// the handshake so the client's UI doesn't need a separate
77    /// "which PC am I?" call.
78    pub pc_id: String,
79}
80
81/// Well-known feature flag names. New flags are added here so the
82/// agent + client + dispatcher all share one source of truth. SPEC
83/// §2.12.6 says optional methods MUST be gated via these flags so
84/// older clients/agents degrade gracefully.
85pub mod features {
86    pub const PUSH_NOTIFICATIONS: &str = "push.notifications";
87    pub const PUSH_JOBS: &str = "push.jobs";
88    pub const PUSH_STATE: &str = "push.state";
89    pub const SUPPORT_DIAGNOSTICS: &str = "support.diagnostics";
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn handshake_params_round_trips_through_json() {
98        let p = HandshakeParams {
99            client: "kanade-client".into(),
100            client_version: "0.1.0".into(),
101            protocol: vec![PROTOCOL_V1],
102            features: vec![
103                features::PUSH_NOTIFICATIONS.into(),
104                features::PUSH_JOBS.into(),
105                features::PUSH_STATE.into(),
106            ],
107        };
108        let json = serde_json::to_string(&p).unwrap();
109        let back: HandshakeParams = serde_json::from_str(&json).unwrap();
110        assert_eq!(back.client, "kanade-client");
111        assert_eq!(back.client_version, "0.1.0");
112        assert_eq!(back.protocol, vec![PROTOCOL_V1]);
113        assert!(back.features.contains(&"push.notifications".to_string()));
114    }
115
116    #[test]
117    fn handshake_params_accepts_missing_features() {
118        // Older / lighter clients may omit `features` entirely. The
119        // serde default keeps the decode green.
120        let wire = r#"{"client":"kanade-client","client_version":"0.0.1","protocol":[1]}"#;
121        let p: HandshakeParams = serde_json::from_str(wire).unwrap();
122        assert!(p.features.is_empty());
123    }
124
125    #[test]
126    fn handshake_result_spec_example_decodes() {
127        // Verbatim from SPEC §2.12.6 — pinned so a struct rename
128        // can't silently break the documented contract.
129        let wire = r#"{
130            "protocol": 1,
131            "agent_version": "0.4.0",
132            "features": ["push.notifications","push.jobs","push.state","support.diagnostics"],
133            "session": {"user":"DOMAIN\\alice","session_id":2,"pc_id":"PC1234"}
134        }"#;
135        let r: HandshakeResult = serde_json::from_str(wire).expect("decode");
136        assert_eq!(r.protocol, 1);
137        assert_eq!(r.agent_version, "0.4.0");
138        assert_eq!(r.features.len(), 4);
139        assert_eq!(r.session.user, "DOMAIN\\alice");
140        assert_eq!(r.session.session_id, 2);
141        assert_eq!(r.session.pc_id, "PC1234");
142    }
143
144    #[test]
145    fn handshake_protocol_negotiation_supports_multiple_versions() {
146        // When v2 lands, clients will advertise `protocol:[1,2]`.
147        // The struct must accept multi-element arrays today so the
148        // upgrade path doesn't need a wire change.
149        let wire = r#"{"client":"c","client_version":"0.0.1","protocol":[1,2,3]}"#;
150        let p: HandshakeParams = serde_json::from_str(wire).unwrap();
151        assert_eq!(p.protocol, vec![1, 2, 3]);
152    }
153}