1#![forbid(unsafe_code)]
9
10use std::io::{BufRead, BufReader};
11use std::process::{Command, Stdio};
12use std::time::Duration;
13
14use touchstone_core::{
15 verdict_for, Attestation, CheckResult, Organ, Status, Subject, SPEC_VERSION,
16};
17
18#[derive(Debug, thiserror::Error)]
20pub enum HarnessError {
21 #[error("adapter failed to spawn: {0}")]
23 Spawn(std::io::Error),
24 #[error("adapter exited with status {0}")]
26 ExitStatus(std::process::ExitStatus),
27 #[error("adapter exceeded timeout of {0:?}")]
29 Timeout(std::time::Duration),
30 #[error("adapter produced no check results")]
32 NoResults,
33 #[error("invalid protocol line: {0}")]
35 Protocol(String),
36}
37
38pub trait Adapter {
40 fn name(&self) -> &str;
42 fn run(&self) -> Vec<CheckResult>;
44}
45
46#[derive(serde::Deserialize)]
48struct ProtocolLine {
49 check: String,
50 organ: String,
51 status: String,
52 #[serde(default)]
53 evidence: serde_json::Value,
54 #[serde(default)]
55 control: bool,
56}
57
58fn parse_organ(s: &str) -> Option<Organ> {
59 Some(match s {
60 "awake" => Organ::Awake,
61 "identity" => Organ::Identity,
62 "perception" => Organ::Perception,
63 "memory" => Organ::Memory,
64 "deliberation" => Organ::Deliberation,
65 "action" => Organ::Action,
66 "vigilance" => Organ::Vigilance,
67 "learning" => Organ::Learning,
68 "audit" => Organ::Audit,
69 "sovereignty" => Organ::Sovereignty,
70 _ => return None,
71 })
72}
73
74fn parse_status(s: &str, control: bool) -> Option<Status> {
75 Some(match s {
76 "pass" => Status::Pass,
77 "fail" => {
78 if control {
79 Status::ControlOk
80 } else {
81 Status::Fail
82 }
83 }
84 "control_ok" => Status::ControlOk,
85 "optional" | "skip" => Status::Optional,
86 _ => return None,
87 })
88}
89
90fn parse_line(line: &str) -> Option<CheckResult> {
92 let trimmed = line.trim();
93 if trimmed.is_empty() || !trimmed.starts_with('{') {
94 return None;
95 }
96 let p: ProtocolLine = serde_json::from_str(trimmed).ok()?;
97 let organ = parse_organ(&p.organ)?;
98 let status = parse_status(&p.status, p.control)?;
99 Some(CheckResult {
100 id: p.check,
101 organ,
102 status,
103 evidence: p.evidence,
104 control: p.control,
105 })
106}
107
108pub struct ExecAdapter {
110 pub program: String,
112 pub args: Vec<String>,
114 pub timeout: Duration,
116}
117
118impl ExecAdapter {
119 pub fn new(program: impl Into<String>) -> Self {
122 Self {
123 program: program.into(),
124 args: Vec::new(),
125 timeout: Duration::from_secs(600),
126 }
127 }
128
129 pub fn collect(&self) -> Result<Vec<CheckResult>, HarnessError> {
135 let mut child = Command::new(&self.program)
136 .args(&self.args)
137 .stdout(Stdio::piped())
138 .stderr(Stdio::inherit())
139 .spawn()
140 .map_err(HarnessError::Spawn)?;
141
142 let stdout = child.stdout.take().expect("stdout piped");
143 let (tx, rx) = std::sync::mpsc::channel();
145 let reader = std::thread::spawn(move || {
146 for line in BufReader::new(stdout).lines() {
147 match line {
148 Ok(l) => {
149 if tx.send(l).is_err() {
150 return;
151 }
152 }
153 Err(_) => return,
154 }
155 }
156 });
157
158 let deadline = std::time::Instant::now() + self.timeout;
159 let mut results = Vec::new();
160 loop {
161 match rx.recv_timeout(Duration::from_millis(100)) {
162 Ok(line) => {
163 if let Some(r) = parse_line(&line) {
164 results.push(r);
165 }
166 }
167 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
168 if std::time::Instant::now() > deadline {
169 let _ = child.kill();
170 let _ = child.wait();
171 return Err(HarnessError::Timeout(self.timeout));
172 }
173 match child.try_wait() {
174 Ok(Some(_)) => {
175 for line in rx.try_iter() {
177 if let Some(r) = parse_line(&line) {
178 results.push(r);
179 }
180 }
181 break;
182 }
183 Ok(None) => {}
184 Err(_) => break,
185 }
186 }
187 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
188 }
189 }
190 let _ = reader.join();
191
192 let status = child.wait().map_err(HarnessError::Spawn)?;
193 if !status.success() {
194 return Err(HarnessError::ExitStatus(status));
195 }
196 if results.is_empty() {
197 return Err(HarnessError::NoResults);
198 }
199 Ok(results)
200 }
201}
202
203pub fn attest(
205 subject: Subject,
206 checks: Vec<CheckResult>,
207 timestamp: impl Into<String>,
208) -> Attestation {
209 let verdict = verdict_for(&checks);
210 Attestation {
211 spec: format!("touchstone/{SPEC_VERSION}"),
212 subject,
213 timestamp: timestamp.into(),
214 checks,
215 verdict,
216 signature: None,
217 }
218}
219
220pub const SPEC_CHECKS: &[(&str, Organ, &str)] = &[
222 (
223 "awake.process",
224 Organ::Awake,
225 "persistent process reports liveness",
226 ),
227 (
228 "identity.declare",
229 Organ::Identity,
230 "declares what it is, bound to a device key",
231 ),
232 (
233 "perception.primary",
234 Organ::Perception,
235 "first opt-in sense reads a planted stimulus",
236 ),
237 (
238 "perception.secondary",
239 Organ::Perception,
240 "second opt-in sense reads a planted stimulus",
241 ),
242 (
243 "memory.store_recall",
244 Organ::Memory,
245 "stores a token and recalls it later",
246 ),
247 (
248 "memory.continuity",
249 Organ::Memory,
250 "history persists across days/restarts",
251 ),
252 (
253 "deliberation.record",
254 Organ::Deliberation,
255 "structured decision procedure produces a recorded verdict",
256 ),
257 (
258 "action.tool_ledgered",
259 Organ::Action,
260 "a tool execution lands on the audit record",
261 ),
262 (
263 "vigilance.watcher",
264 Organ::Vigilance,
265 "a watcher or standing order fires unprompted",
266 ),
267 (
268 "learning.self_improve",
269 Organ::Learning,
270 "weight/strategy-level self-improvement evidence",
271 ),
272 (
273 "audit.chain_valid",
274 Organ::Audit,
275 "hash-chained log verifies independently",
276 ),
277 (
278 "audit.control_negative",
279 Organ::Audit,
280 "planted failure reports FAIL (control check)",
281 ),
282 (
283 "sovereignty.no_egress",
284 Organ::Sovereignty,
285 "no required external network sockets",
286 ),
287 (
288 "sovereignty.kill_path",
289 Organ::Sovereignty,
290 "owner-held stop path exists",
291 ),
292];
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use touchstone_core::{CheckResult as CR, Verdict};
298
299 struct Toy;
300 impl Adapter for Toy {
301 fn name(&self) -> &'static str {
302 "toy"
303 }
304 fn run(&self) -> Vec<CR> {
305 Organ::ALL
306 .iter()
307 .map(|o| CR {
308 id: "t".into(),
309 organ: *o,
310 status: Status::Pass,
311 evidence: serde_json::Value::Null,
312 control: false,
313 })
314 .collect()
315 }
316 }
317
318 #[test]
319 fn toy_adapter_conforms() {
320 let checks = Toy.run();
321 let doc = attest(
322 Subject {
323 name: "toy".into(),
324 version: "0".into(),
325 host: "h".into(),
326 },
327 checks,
328 "now",
329 );
330 assert_eq!(doc.verdict, Verdict::Conformant);
331 assert!(doc.spec.starts_with("touchstone/"));
332 }
333
334 #[test]
335 fn protocol_parsing() {
336 assert_eq!(parse_organ("perception"), Some(Organ::Perception));
337 assert_eq!(parse_organ("nope"), None);
338 assert_eq!(parse_status("fail", true), Some(Status::ControlOk));
339 assert_eq!(parse_status("fail", false), Some(Status::Fail));
340 assert_eq!(parse_status("optional", false), Some(Status::Optional));
341 }
342}