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