clawft_kernel/workload_runtime/
host_contract.rs1use std::net::SocketAddr;
9
10use clawft_types::secret::SecretString;
11use rand::RngCore;
12
13use super::types::RuntimeError;
14
15pub const ENV_CSI_BIND: &str = "COG_CSI_BIND";
17pub const ENV_SENSOR_URL: &str = "COG_SENSOR_URL";
19pub const ENV_TOKEN: &str = "COGNITUM_COG_TOKEN";
21pub const ENV_INGEST_URL: &str = "COGNITUM_INGEST_URL";
24pub const ENV_DATA_DIR: &str = "COGNITUM_COG_DATA_DIR";
26pub const DEFAULT_CSI_PORT: u16 = 5006;
28
29#[derive(Debug, Clone)]
31pub struct HostContract {
32 pub csi_bind: SocketAddr,
34 pub sensor_url: Option<SocketAddr>,
36 pub token: SecretString,
38 pub ingest_upstream: Option<SocketAddr>,
43 pub ingest_url: Option<String>,
45 pub expose_token: bool,
49}
50
51impl HostContract {
52 pub fn new(csi_bind: SocketAddr) -> Self {
54 Self {
55 csi_bind,
56 sensor_url: None,
57 token: Self::fresh_token(),
58 ingest_upstream: None,
59 ingest_url: None,
60 expose_token: true,
61 }
62 }
63
64 pub fn default_feed() -> Self {
66 Self::new(SocketAddr::from(([0, 0, 0, 0], DEFAULT_CSI_PORT)))
67 }
68
69 pub fn with_sensor(mut self, addr: SocketAddr) -> Self {
71 self.sensor_url = Some(addr);
72 self
73 }
74
75 pub fn with_ingest_upstream(mut self, addr: SocketAddr) -> Self {
77 self.ingest_upstream = Some(addr);
78 self
79 }
80
81 pub fn with_ingest_url(mut self, url: impl Into<String>) -> Self {
83 self.ingest_url = Some(url.into());
84 self
85 }
86
87 pub fn without_ingest(mut self) -> Self {
89 self.expose_token = false;
90 self.ingest_url = None;
91 self
92 }
93
94 pub fn fresh_token() -> SecretString {
96 let mut b = [0u8; 32];
97 rand::thread_rng().fill_bytes(&mut b);
98 SecretString::new(crate::workload_pkg::codec::hex_encode(&b))
99 }
100
101 pub fn token_hash(&self) -> String {
103 blake3::hash(self.token.expose().as_bytes()).to_hex()[..16].to_string()
104 }
105
106 pub fn validate(&self) -> Result<(), RuntimeError> {
108 if self.csi_bind.port() == 0 {
109 return Err(RuntimeError::InvalidConfig("csi_bind needs a port".into()));
110 }
111 let t = self.token.expose();
112 if t.len() < 32 || t.len() > 256 || !t.bytes().all(|b| b.is_ascii_graphic()) {
113 return Err(RuntimeError::InvalidConfig(
114 "cog token must be 32..256 printable characters".into(),
115 ));
116 }
117 Ok(())
118 }
119
120 pub fn env(&self, data_dir: &str) -> Vec<(String, String)> {
123 let mut env = vec![
124 (ENV_CSI_BIND.to_string(), self.csi_bind.to_string()),
125 (ENV_DATA_DIR.to_string(), data_dir.to_string()),
126 ];
127 if self.expose_token {
128 env.push((ENV_TOKEN.to_string(), self.token.expose().to_string()));
129 }
130 if let Some(s) = self.sensor_url {
131 env.push((ENV_SENSOR_URL.to_string(), s.to_string()));
132 }
133 if let Some(u) = &self.ingest_url {
134 env.push((ENV_INGEST_URL.to_string(), u.clone()));
135 }
136 env
137 }
138
139 pub fn audit(&self) -> serde_json::Value {
141 serde_json::json!({
142 "csi_bind": self.csi_bind.to_string(),
143 "sensor_url": self.sensor_url.map(|s| s.to_string()),
144 "token_hash": self.token_hash(),
145 "ingest_upstream": self.ingest_upstream.map(|s| s.to_string()),
146 "ingest_url": self.ingest_url,
147 })
148 }
149}