1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
11#[jumo(kind = "struct", domain = "Control", module = "Control.Agent.Identity")]
12#[serde(deny_unknown_fields)]
13pub struct HostProfile {
14 pub node_id: String,
15 pub hostname: String,
16 pub os: String,
17 pub arch: String,
18 pub machine_id: String,
19 pub cloud_instance_id: Option<String>,
20 pub k8s_node_uid: Option<String>,
21 pub ip_addresses: Vec<String>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
25#[jumo(kind = "struct", domain = "Control", module = "Control.Agent.Identity")]
26#[serde(deny_unknown_fields)]
27pub struct AgentIdentity {
28 pub agent_id: String,
29 pub instance_id: String,
30 pub tenant_id: String,
31 pub environment_id: String,
32 pub node_id: String,
33 pub issued_at: String,
34 pub expires_at: Option<String>,
35 pub status: AgentIdentityStatus,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
39#[jumo(kind = "state", domain = "Control", module = "Control.Agent.Identity")]
40pub enum AgentIdentityStatus {
41 #[serde(rename = "active")]
42 Active,
43 #[serde(rename = "revoked")]
44 Revoked,
45 #[serde(rename = "expired")]
46 Expired,
47 #[serde(rename = "renewal_required")]
48 RenewalRequired,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
52#[jumo(kind = "struct", domain = "Control", module = "Control.Agent.Identity")]
53#[serde(deny_unknown_fields)]
54pub struct CredentialBundle {
55 pub credential_id: String,
56 pub agent_id: String,
57 pub instance_id: String,
58 pub certificate: String,
61 pub private_key_ref: Option<String>,
62 pub ca_bundle: Option<String>,
63 pub issued_at: String,
64 pub not_before: Option<String>,
65 pub not_after: Option<String>,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct InitialConfig {
71 pub schema_version: String,
72 pub mode: String,
73 pub gateway_endpoint: String,
74 pub policy_version: String,
75 pub telemetry_output: Option<String>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct PolicyBinding {
81 pub agent_id: String,
82 pub policy_id: String,
83 pub policy_version: String,
84 pub bound_at: String,
85}
86
87#[cfg(test)]
88mod tests {
89 use super::{CredentialBundle, HostProfile};
90
91 #[test]
94 fn credential_bundle_carries_only_the_client_certificate() {
95 let bundle = CredentialBundle {
96 credential_id: "cred-1".to_string(),
97 agent_id: "agent-1".to_string(),
98 instance_id: "inst-1".to_string(),
99 certificate: "CERT".to_string(),
100 private_key_ref: None,
101 ca_bundle: None,
102 issued_at: "2026-09-30T00:00:00Z".to_string(),
103 not_before: None,
104 not_after: None,
105 };
106 let json = serde_json::to_string(&bundle).expect("encode");
107 assert!(!json.contains("bearer_token"), "{json}");
108 assert!(!json.contains("auth_scheme"), "{json}");
109
110 let legacy = r#"{"credential_id":"c","agent_id":"a","instance_id":"i",\
112 "auth_scheme":"bearer","bearer_token":"wic_x","certificate":"CERT",\
113 "private_key_ref":null,"ca_bundle":null,"issued_at":"t",\
114 "not_before":null,"not_after":null}"#;
115 assert!(serde_json::from_str::<CredentialBundle>(legacy).is_err());
116
117 let no_certificate = r#"{"credential_id":"c","agent_id":"a","instance_id":"i",\
119 "private_key_ref":null,"ca_bundle":null,"issued_at":"t",\
120 "not_before":null,"not_after":null}"#;
121 assert!(serde_json::from_str::<CredentialBundle>(no_certificate).is_err());
122 }
123
124 #[test]
125 fn host_profile_round_trips_and_rejects_unknown_fields() {
126 let profile = HostProfile {
127 node_id: "node-1".to_string(),
128 hostname: "host-1".to_string(),
129 os: "macos".to_string(),
130 arch: "arm64".to_string(),
131 machine_id: "mid-1".to_string(),
132 cloud_instance_id: None,
133 k8s_node_uid: None,
134 ip_addresses: vec!["10.0.0.1".to_string()],
135 };
136 let json = serde_json::to_string(&profile).expect("encode");
137 let back: HostProfile = serde_json::from_str(&json).expect("decode");
138 assert_eq!(back, profile);
139
140 let mutated = json.replacen('{', "{\"extra\":1,", 1);
141 assert!(serde_json::from_str::<HostProfile>(&mutated).is_err());
142 }
143}