1use std::io::{Read, Write};
2use std::process::{Child, Command, Stdio};
3use std::collections::BTreeMap;
4
5#[derive(Debug, thiserror::Error)]
6pub enum HostError {
7 #[error("IO error: {0}")]
8 Io(#[from] std::io::Error),
9 #[error("Plugin crashed or closed connection")]
10 Crashed,
11 #[error("Plugin produced NaN (NaN storm)")]
12 NanStorm,
13 #[error("Serialization error: {0}")]
14 Serialization(#[from] serde_json::Error),
15}
16#[derive(Debug, serde::Serialize)]
17pub struct AuditReport {
18 pub root_hash_valid: bool,
19 pub manifest_valid: bool,
20 pub lineage_intact: bool,
21 pub cas_complete: bool,
22 pub determinism_verified: bool,
23 pub issues: Vec<String>,
24}
25
26impl AuditReport {
27 pub fn is_healthy(&self) -> bool {
28 self.root_hash_valid && self.lineage_intact && self.cas_complete
29 }
30}
31
32use std::path::{Path, PathBuf};
33use serde::Serialize;
34
35pub struct Workspace {
38 root: PathBuf,
39 graph: dirtydata_core::ir::Graph,
40 intent_state: dirtydata_intent::IntentState,
41}
42
43impl Workspace {
44 pub fn open(root: impl Into<PathBuf>) -> Result<Self, HostError> {
45 let root = root.into();
46 let dot_dirty = root.join(".dirtydata");
47
48 if !dot_dirty.exists() {
49 std::fs::create_dir_all(&dot_dirty)?;
50 return Ok(Self { root, graph: dirtydata_core::ir::Graph::new(), intent_state: dirtydata_intent::IntentState::default() });
51 }
52
53 let manifest_path = dot_dirty.join("manifest.json");
55 let manifest: dirtydata_core::types::Manifest = if manifest_path.exists() {
56 serde_json::from_str(&std::fs::read_to_string(manifest_path)?)?
57 } else {
58 return Err(HostError::Crashed); };
61
62 let topo_path = dot_dirty.join("topology.ir");
64 let topology: dirtydata_core::ir::Topology = serde_json::from_str(&std::fs::read_to_string(topo_path)?)?;
65
66 let lineage_path = dot_dirty.join("lineage.dag");
68 let lineage: dirtydata_core::ir::Lineage = serde_json::from_str(&std::fs::read_to_string(lineage_path)?)?;
69
70 let mut registry = dirtydata_core::ir::CircuitRegistry::default();
72 let cas_root = dot_dirty.join("circuits").join("blake3");
73 if cas_root.exists() {
74 for entry in walkdir::WalkDir::new(cas_root) {
76 let entry = entry.map_err(|_| HostError::Crashed)?;
77 if entry.file_type().is_file() {
78 let data = std::fs::read_to_string(entry.path())?;
79 let def: dirtydata_core::types::CircuitDefinition = serde_json::from_str(&data)?;
80 registry.definitions.insert(def.id, def);
81 }
82 }
83 }
84
85 let mut graph = dirtydata_core::ir::Graph {
86 spec_version: manifest.spec_version,
87 topology,
88 lineage,
89 registry,
90 verification: manifest.verification,
91 revision: dirtydata_core::types::Revision(manifest.last_revision),
92 nodes: BTreeMap::new(),
93 edges: BTreeMap::new(),
94 modulations: BTreeMap::new(),
95 };
96 graph.sync();
97
98 let intent_state = dirtydata_intent::IntentState::load(&root)?;
99
100 Ok(Self { root, graph, intent_state })
101 }
102
103 pub fn save(&self) -> Result<(), HostError> {
105 let dot_dirty = self.root.join(".dirtydata");
106 std::fs::create_dir_all(&dot_dirty)?;
107
108 self.save_atomic(&dot_dirty.join("topology.ir"), &self.graph.topology)?;
110
111 for def in self.graph.registry.definitions.values() {
113 let hash = def.hash();
114 let hash_hex = hex::encode(hash);
115 let cas_path = dot_dirty.join("circuits").join("blake3")
116 .join(&hash_hex[0..2])
117 .join(&hash_hex[2..4]);
118 std::fs::create_dir_all(&cas_path)?;
119 self.save_atomic(&cas_path.join(&hash_hex), def)?;
120 }
121
122 self.save_atomic(&dot_dirty.join("lineage.dag"), &self.graph.lineage)?;
124
125 self.intent_state.save(&self.root)?;
127
128 let root_hash = self.calculate_root_hash()?;
130 let manifest = dirtydata_core::types::Manifest {
131 spec_version: self.graph.spec_version.clone(),
132 last_revision: self.graph.revision.0,
133 timestamp: dirtydata_core::types::Timestamp::now().0,
134 verification: dirtydata_core::types::Verification {
135 null_test: true,
136 hash: hex::encode(root_hash),
137 trust_state: "verified".into(),
138 },
139 author_id: "dirtydata-host-local".into(),
140 public_key: "ed25519:stub_key".into(),
141 signature: "stub_signature".into(),
142 };
143 self.save_atomic(&dot_dirty.join("manifest.json"), &manifest)?;
144
145 tracing::info!("Forensic record (Merkle DAG) saved successfully. Root Hash: {}", hex::encode(root_hash));
146 Ok(())
147 }
148
149 pub fn calculate_root_hash(&self) -> Result<dirtydata_core::types::Hash, HostError> {
150 let mut hasher = blake3::Hasher::new();
151 hasher.update(serde_json::to_string(&self.graph.topology)?.as_bytes());
152 hasher.update(serde_json::to_string(&self.graph.lineage)?.as_bytes());
154 hasher.update(serde_json::to_string(&self.intent_state)?.as_bytes());
155
156 if let Some(&last_id) = self.graph.lineage.applied_patches.last() {
158 if let Some(patch) = self.graph.lineage.history.get(&last_id) {
159 hasher.update(&patch.deterministic_hash);
160 }
161 }
162
163 for def in self.graph.registry.definitions.values() {
164 hasher.update(&def.hash());
165 }
166 Ok(*hasher.finalize().as_bytes())
167 }
168
169 fn save_atomic<T: Serialize>(&self, path: &Path, data: &T) -> Result<(), HostError> {
170 let mut temp = tempfile::NamedTempFile::new_in(path.parent().unwrap())?;
171 let json = serde_json::to_string_pretty(data)?;
172 temp.write_all(json.as_bytes())?;
173 temp.persist(path).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
174 Ok(())
175 }
176
177 pub fn graph(&self) -> &dirtydata_core::ir::Graph {
180 &self.graph
181 }
182
183 pub fn graph_mut(&mut self) -> &mut dirtydata_core::ir::Graph {
184 &mut self.graph
185 }
186
187 pub fn intent_state(&self) -> &dirtydata_intent::IntentState {
188 &self.intent_state
189 }
190
191 pub fn root(&self) -> &Path {
192 &self.root
193 }
194
195 pub fn apply_patch(&mut self, patch: dirtydata_core::patch::Patch) -> Result<(), HostError> {
198 self.graph.apply_patch(&patch)
199 .map_err(|_| HostError::Crashed)?; self.save()?;
201 Ok(())
202 }
203
204 pub fn audit(&self) -> Result<AuditReport, HostError> {
206 let mut report = AuditReport {
207 root_hash_valid: false,
208 manifest_valid: true, lineage_intact: true,
210 cas_complete: true,
211 determinism_verified: false,
212 issues: Vec::new(),
213 };
214
215 let actual_hash = self.calculate_root_hash()?;
217 let manifest_path = self.root.join(".dirtydata").join("manifest.json");
218 if manifest_path.exists() {
219 let manifest: dirtydata_core::types::Manifest = serde_json::from_str(&std::fs::read_to_string(manifest_path)?)?;
220 if hex::encode(actual_hash) == manifest.verification.hash {
221 report.root_hash_valid = true;
222 } else {
223 report.issues.push(format!("Root hash mismatch! Manifest: {}, Actual: {}", manifest.verification.hash, hex::encode(actual_hash)));
224 }
225 }
226
227 let mut prev_hash = None;
229 for patch_id in &self.graph.lineage.applied_patches {
230 if let Some(patch) = self.graph.lineage.history.get(patch_id) {
231 if !patch.verify_hash() {
232 report.lineage_intact = false;
233 report.issues.push(format!("Patch {} has corrupted hash", patch_id));
234 }
235 if let Some(p_hash) = prev_hash {
237 if !patch.parent_hashes.contains(&p_hash) {
238 }
241 }
242 prev_hash = Some(patch.deterministic_hash);
243 }
244 }
245
246 for node in self.graph.topology.nodes.values() {
248 if let dirtydata_core::types::NodeKind::Processor = node.kind {
249 }
251 }
252
253 Ok(report)
258 }
259}
260
261#[repr(u8)]
269#[derive(Debug, Clone, Copy)]
270pub enum HostCommand {
271 Process = 0,
272 SetParameter = 1,
273 GetState = 2,
274 SetState = 3,
275}
276
277pub struct PluginHost {
278 child: Child,
279 fallback_buffer: Vec<f32>,
280}
281
282impl PluginHost {
283 pub fn new(plugin_name: &str, buffer_size: usize) -> Result<Self, HostError> {
284 let exe = std::env::current_exe().unwrap_or_default();
285 let dir = exe.parent().unwrap_or(std::path::Path::new("."));
286 let worker_path = dir.join("dirtydata-plugin-worker");
287
288 let child = Command::new(&worker_path)
289 .arg(plugin_name)
290 .stdin(Stdio::piped())
291 .stdout(Stdio::piped())
292 .spawn()
293 .map_err(|e| {
294 tracing::error!("Failed to spawn plugin worker '{}' at {:?}: {}", plugin_name, worker_path, e);
295 e
296 })?;
297
298 tracing::info!("Spawned plugin worker '{}' (pid={})", plugin_name, child.id());
299 Ok(Self {
300 child,
301 fallback_buffer: vec![0.0; buffer_size],
302 })
303 }
304
305 pub fn set_parameter(&mut self, param_id: u32, value: f32) -> Result<(), HostError> {
306 let mut stdin = self.child.stdin.as_ref().ok_or(HostError::Crashed)?;
307
308 let cmd = HostCommand::SetParameter as u8;
309 stdin.write_all(&[cmd])?;
310 stdin.write_all(¶m_id.to_le_bytes())?;
311 stdin.write_all(&value.to_le_bytes())?;
312 stdin.flush()?;
313
314 Ok(())
315 }
316
317 pub fn process(&mut self, input: &[f32], output: &mut [f32]) -> Result<(), HostError> {
318 let mut stdin = self.child.stdin.as_ref().ok_or(HostError::Crashed)?;
319 let stdout = self.child.stdout.as_mut().ok_or(HostError::Crashed)?;
320
321 let cmd = HostCommand::Process as u8;
323 stdin.write_all(&[cmd])?;
324
325 let size = input.len() as u32;
327 stdin.write_all(&size.to_le_bytes())?;
328
329 let in_bytes = bytemuck::cast_slice(input);
331 if stdin.write_all(in_bytes).is_err() {
332 tracing::error!("Failed to write to plugin stdin — plugin likely crashed");
333 return Err(HostError::Crashed);
334 }
335 if stdin.flush().is_err() {
336 tracing::error!("Failed to flush plugin stdin");
337 return Err(HostError::Crashed);
338 }
339
340 let out_bytes = bytemuck::cast_slice_mut(output);
342 if stdout.read_exact(out_bytes).is_err() {
343 return Err(HostError::Crashed);
344 }
345
346 for sample in output.iter() {
348 if sample.is_nan() {
349 tracing::warn!("NaN detected in plugin output! Entering NaN storm protocol.");
350 return Err(HostError::NanStorm);
351 }
352 }
353
354 if self.fallback_buffer.len() != output.len() {
356 self.fallback_buffer.resize(output.len(), 0.0);
357 }
358 self.fallback_buffer.copy_from_slice(output);
359
360 Ok(())
361 }
362
363 pub fn get_fallback(&self) -> &[f32] {
364 &self.fallback_buffer
365 }
366}