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