Skip to main content

touchstone_core/
lib.rs

1//! touchstone-core: the types and verdict rules for the personal AGI
2//! conformance spec. See SPEC.md at the repo root for the normative text.
3//!
4//! This crate deliberately has no filesystem, network, or process
5//! dependencies so it can compile to WASM and be shared by every verifier.
6
7#![forbid(unsafe_code)]
8
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12/// Version of the conformance spec this crate implements. Independent of
13/// crate semver — SPEC.md bumps only when the checklist or verdict rules
14/// change, not on every code release.
15pub const SPEC_VERSION: &str = "0.2";
16
17/// The organs a conformant organism must demonstrate.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Organ {
21    /// Persistent process exists and reports liveness.
22    Awake,
23    /// Organism declares what it is, bound to a device-held key.
24    Identity,
25    /// Opt-in senses (screen, audio, clipboard, etc.) working end-to-end.
26    Perception,
27    /// Store/recall of arbitrary state across time.
28    Memory,
29    /// Recorded structured decision procedure before consequential action.
30    Deliberation,
31    /// Tool executions landing on the audit record.
32    Action,
33    /// Watchers/standing orders firing without a prompt.
34    Vigilance,
35    /// Measurable self-improvement (adapters, strategy memory, etc.).
36    Learning,
37    /// Hash-chained event log verifiable by a second implementation.
38    Audit,
39    /// No required egress; an owner-held stop path exists.
40    Sovereignty,
41    /// Goal achievement demonstrated across multiple distinct domains.
42    Generality,
43    /// Multi-step task decomposition with recorded plan and replanning.
44    Planning,
45    /// Detecting and correcting the organism's own errors.
46    Reflection,
47}
48
49impl Organ {
50    /// Every required organ, in spec order.
51    pub const ALL: [Organ; 13] = [
52        Organ::Awake,
53        Organ::Identity,
54        Organ::Perception,
55        Organ::Memory,
56        Organ::Deliberation,
57        Organ::Action,
58        Organ::Vigilance,
59        Organ::Learning,
60        Organ::Audit,
61        Organ::Sovereignty,
62        Organ::Generality,
63        Organ::Planning,
64        Organ::Reflection,
65    ];
66}
67
68/// Status of a single check, as emitted by an adapter.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum Status {
72    /// Evidence verified.
73    Pass,
74    /// Evidence absent, invalid, or contradictory.
75    Fail,
76    /// Check intentionally skipped / not applicable to this subject.
77    Optional,
78    /// A control check that failed as designed (proving instrumentation works).
79    ControlOk,
80}
81
82/// One check result in an attestation.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct CheckResult {
85    /// Dotted id, e.g. "perception.screen".
86    pub id: String,
87    /// Which organ this check measures.
88    pub organ: Organ,
89    /// Outcome of the check.
90    pub status: Status,
91    /// Free-form evidence payload — transcripts, hashes, ledger seq ranges.
92    #[serde(default)]
93    pub evidence: serde_json::Value,
94    /// True when this check is a deliberate-failure control.
95    #[serde(default)]
96    pub control: bool,
97}
98
99/// Overall verdict for a run.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "snake_case")]
102pub enum Verdict {
103    /// Every required organ passed and every control failed as designed.
104    Conformant,
105    /// Some required check failed or is missing.
106    Partial,
107    /// No meaningful evidence / harness integrity failure
108    /// (a control check passed, or zero checks ran).
109    Nonconformant,
110}
111
112/// The subject under test.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Subject {
115    /// Organism/implementation name.
116    pub name: String,
117    /// Subject's own version string.
118    pub version: String,
119    /// Host the run executed on.
120    pub host: String,
121}
122
123/// A signature block on an attestation.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct Signature {
126    /// "ed25519" or "secp256r1-se" (Secure Enclave).
127    pub scheme: String,
128    /// Hex-encoded public key.
129    pub pubkey: String,
130    /// Hex-encoded signature over the canonical document hash.
131    pub sig: String,
132}
133
134/// The signed attestation document.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct Attestation {
137    /// Spec tag, e.g. "touchstone/0.1".
138    pub spec: String,
139    /// What was measured.
140    pub subject: Subject,
141    /// ISO-8601 timestamp.
142    pub timestamp: String,
143    /// Every check result the adapter produced.
144    pub checks: Vec<CheckResult>,
145    /// Verdict claimed by the producer — recomputed on verify.
146    pub verdict: Verdict,
147    /// Device-key signature over the canonical document hash.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub signature: Option<Signature>,
150}
151
152/// Compute the verdict for a set of check results.
153///
154/// - `Conformant`: every organ has >=1 `PASS`, no required check `FAIL`ed,
155///   every control check reported `ControlOk` (i.e. it failed as designed).
156/// - `Nonconformant`: a control check passed (instrumentation can't be
157///   trusted) or the run produced no checks at all.
158/// - `Partial`: anything else.
159#[must_use]
160pub fn verdict_for(checks: &[CheckResult]) -> Verdict {
161    if checks.is_empty() {
162        return Verdict::Nonconformant;
163    }
164    // A control that PASSED means the harness can rubber-stamp: nonconformant.
165    for c in checks.iter().filter(|c| c.control) {
166        if c.status == Status::Pass {
167            return Verdict::Nonconformant;
168        }
169    }
170    let mut organs_passed = [false; 13];
171    let mut any_fail = false;
172    for c in checks {
173        match c.status {
174            Status::Pass => {
175                if let Some(i) = Organ::ALL.iter().position(|o| *o == c.organ) {
176                    organs_passed[i] = true;
177                }
178            }
179            // A control check failing is the expected outcome — it proves the
180            // harness can report failure — so it must not count as a real fail.
181            Status::Fail if !c.control => any_fail = true,
182            _ => {}
183        }
184    }
185    if organs_passed.iter().all(|p| *p) && !any_fail {
186        Verdict::Conformant
187    } else {
188        Verdict::Partial
189    }
190}
191
192/// Canonical serialization of an attestation for signing: the JSON with the
193/// signature field removed, keys emitted in sorted order.
194#[must_use]
195pub fn canonical_bytes(doc: &Attestation) -> Vec<u8> {
196    let mut v = serde_json::to_value(doc).expect("attestation serializes");
197    if let Some(obj) = v.as_object_mut() {
198        obj.remove("signature");
199    }
200    canonical_json(&v).into_bytes()
201}
202
203/// SHA-256 of the canonical document — what signatures cover.
204#[must_use]
205pub fn document_hash(doc: &Attestation) -> [u8; 32] {
206    let mut h = Sha256::new();
207    h.update(canonical_bytes(doc));
208    h.finalize().into()
209}
210
211/// Structural validation errors an attestation can carry even when it
212/// parses as JSON.
213#[derive(Debug)]
214pub enum ValidationError {
215    /// `spec` field missing or not a `touchstone/<version>` string.
216    BadSpec,
217    /// No checks present — nothing was actually measured.
218    NoChecks,
219    /// Two checks share an id, making the scoreboard ambiguous.
220    DuplicateCheckId(String),
221    /// Claimed verdict disagrees with the verdict recomputed from checks.
222    VerdictMismatch {
223        /// Verdict the document claims.
224        claimed: Verdict,
225        /// Verdict the check results actually produce.
226        actual: Verdict,
227    },
228    /// Signature present but malformed (bad hex, wrong length).
229    BadSignature,
230    /// Timestamp missing or not RFC3339-parseable.
231    BadTimestamp,
232}
233
234impl std::fmt::Display for ValidationError {
235    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236        match self {
237            Self::BadSpec => write!(f, "spec field missing or not touchstone/<version>"),
238            Self::NoChecks => write!(f, "attestation has no checks"),
239            Self::DuplicateCheckId(id) => write!(f, "duplicate check id: {id}"),
240            Self::VerdictMismatch { claimed, actual } => {
241                write!(f, "claimed verdict {claimed:?} != recomputed {actual:?}")
242            }
243            Self::BadSignature => write!(f, "signature malformed"),
244            Self::BadTimestamp => write!(f, "timestamp missing or not RFC3339"),
245        }
246    }
247}
248
249impl std::error::Error for ValidationError {}
250
251impl Attestation {
252    /// Validate structure independent of signature: spec tag, check
253    /// uniqueness, timestamp shape, and that the claimed verdict equals the
254    /// verdict the checks actually produce. Does NOT verify the signature —
255    /// that is the identity crate's job.
256    pub fn validate(&self) -> Result<(), Vec<ValidationError>> {
257        let mut errs = Vec::new();
258        if !self.spec.starts_with("touchstone/") || self.spec.len() <= "touchstone/".len() {
259            errs.push(ValidationError::BadSpec);
260        }
261        if self.checks.is_empty() {
262            errs.push(ValidationError::NoChecks);
263        }
264        let mut seen = std::collections::HashSet::new();
265        for c in &self.checks {
266            if !seen.insert(&c.id) {
267                errs.push(ValidationError::DuplicateCheckId(c.id.clone()));
268            }
269        }
270        let actual = verdict_for(&self.checks);
271        if actual != self.verdict {
272            errs.push(ValidationError::VerdictMismatch {
273                claimed: self.verdict,
274                actual,
275            });
276        }
277        if let Some(sig) = &self.signature {
278            let hex_ok =
279                |s: &str, n: usize| s.len() == n && s.chars().all(|c| c.is_ascii_hexdigit());
280            if !hex_ok(&sig.pubkey, 64) || !hex_ok(&sig.sig, 128) {
281                errs.push(ValidationError::BadSignature);
282            }
283        }
284        if chrono::DateTime::parse_from_rfc3339(&self.timestamp).is_err() {
285            errs.push(ValidationError::BadTimestamp);
286        }
287        if errs.is_empty() {
288            Ok(())
289        } else {
290            Err(errs)
291        }
292    }
293}
294
295/// Serialize a JSON value with all object keys sorted (canonical form).
296fn canonical_json(v: &serde_json::Value) -> String {
297    match v {
298        serde_json::Value::Object(map) => {
299            let mut keys: Vec<&String> = map.keys().collect();
300            keys.sort();
301            let inner: Vec<String> = keys
302                .into_iter()
303                .map(|k| {
304                    format!(
305                        "{}:{}",
306                        serde_json::to_string(k).unwrap(),
307                        canonical_json(&map[k])
308                    )
309                })
310                .collect();
311            format!("{{{}}}", inner.join(","))
312        }
313        serde_json::Value::Array(a) => {
314            let inner: Vec<String> = a.iter().map(canonical_json).collect();
315            format!("[{}]", inner.join(","))
316        }
317        other => serde_json::to_string(other).unwrap(),
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    fn check(id: &str, organ: Organ, status: Status) -> CheckResult {
326        CheckResult {
327            id: id.into(),
328            organ,
329            status,
330            evidence: serde_json::Value::Null,
331            control: false,
332        }
333    }
334
335    #[test]
336    fn conformant_when_all_organs_pass() {
337        let checks: Vec<CheckResult> = Organ::ALL
338            .iter()
339            .map(|o| check("x", *o, Status::Pass))
340            .collect();
341        assert_eq!(verdict_for(&checks), Verdict::Conformant);
342    }
343
344    #[test]
345    fn partial_when_an_organ_missing() {
346        let checks: Vec<CheckResult> = Organ::ALL[..9]
347            .iter()
348            .map(|o| check("x", *o, Status::Pass))
349            .collect();
350        assert_eq!(verdict_for(&checks), Verdict::Partial);
351    }
352
353    #[test]
354    fn nonconformant_when_control_passes() {
355        let mut checks: Vec<CheckResult> = Organ::ALL
356            .iter()
357            .map(|o| check("x", *o, Status::Pass))
358            .collect();
359        checks.push(CheckResult {
360            control: true,
361            ..check("planted", Organ::Audit, Status::Pass)
362        });
363        assert_eq!(verdict_for(&checks), Verdict::Nonconformant);
364    }
365
366    #[test]
367    fn control_fail_is_fine() {
368        let mut checks: Vec<CheckResult> = Organ::ALL
369            .iter()
370            .map(|o| check("x", *o, Status::Pass))
371            .collect();
372        checks.push(CheckResult {
373            control: true,
374            ..check("planted", Organ::Audit, Status::Fail)
375        });
376        assert_eq!(verdict_for(&checks), Verdict::Conformant);
377    }
378
379    #[test]
380    fn empty_is_nonconformant() {
381        assert_eq!(verdict_for(&[]), Verdict::Nonconformant);
382    }
383
384    #[test]
385    fn canonical_hash_stable() {
386        let doc = Attestation {
387            spec: "touchstone/0.1".into(),
388            subject: Subject {
389                name: "x".into(),
390                version: "0".into(),
391                host: "h".into(),
392            },
393            timestamp: "t".into(),
394            checks: vec![check("a.b", Organ::Awake, Status::Pass)],
395            verdict: Verdict::Partial,
396            signature: None,
397        };
398        assert_eq!(document_hash(&doc), document_hash(&doc));
399        // signature field excluded from hash
400        let mut signed = doc.clone();
401        signed.signature = Some(Signature {
402            scheme: "ed25519".into(),
403            pubkey: "00".into(),
404            sig: "ff".into(),
405        });
406        assert_eq!(document_hash(&doc), document_hash(&signed));
407    }
408}