tdln_gate/
lib.rs

1//! TDLN Policy Gate — preflight & decision, proof-carrying, deterministic.
2//!
3//! Event sequence (high-level): nl.utterance → plan.proposed → policy.preflight
4//! → user.consent → policy.decision → effect.exec → state.update
5
6#![forbid(unsafe_code)]
7
8use serde::{Deserialize, Serialize};
9use tdln_compiler::CompiledIntent;
10use thiserror::Error;
11
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13pub enum Decision {
14    Allow,
15    Deny,
16    NeedsConsent,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct LogEvent {
21    pub kind: String,
22    pub payload: serde_json::Value,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct GateOutput {
27    pub decision: Decision,
28    pub audit: serde_json::Value,
29    pub proof_ref: [u8; 32],
30    pub events: Vec<LogEvent>,
31}
32
33#[derive(Debug, Error)]
34pub enum GateError {
35    #[error("invalid input")]
36    Invalid,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct PolicyCtx {
41    pub allow_freeform: bool,
42}
43
44pub fn preflight(intent: &CompiledIntent, _ctx: &PolicyCtx) -> Result<GateOutput, GateError> {
45    // Minimal deterministic audit
46    let audit = serde_json::json!({
47        "ast_cid": hex::encode(intent.proof.ast_cid),
48        "canon_cid": hex::encode(intent.proof.canon_cid),
49    });
50    let events = vec![LogEvent {
51        kind: "policy.preflight".into(),
52        payload: audit.clone(),
53    }];
54    Ok(GateOutput {
55        decision: Decision::NeedsConsent,
56        audit,
57        proof_ref: intent.cid,
58        events,
59    })
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Consent {
64    pub accepted: bool,
65}
66
67pub fn decide(
68    intent: &CompiledIntent,
69    consent: &Consent,
70    ctx: &PolicyCtx,
71) -> Result<GateOutput, GateError> {
72    let mut out = preflight(intent, ctx)?;
73    let decision = if consent.accepted && ctx.allow_freeform {
74        Decision::Allow
75    } else if !consent.accepted {
76        Decision::NeedsConsent
77    } else {
78        Decision::Deny
79    };
80    out.events.push(LogEvent {
81        kind: "policy.decision".into(),
82        payload: serde_json::json!({ "decision": format!("{:?}", decision) }),
83    });
84    out.decision = decision;
85    Ok(out)
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use tdln_compiler::{compile, CompileCtx};
92    #[test]
93    fn pipeline() {
94        let ctx = CompileCtx {
95            rule_set: "v1".into(),
96        };
97        let compiled = compile("hello world", &ctx).unwrap();
98        let gctx = PolicyCtx {
99            allow_freeform: true,
100        };
101        let pf = preflight(&compiled, &gctx).unwrap();
102        assert_eq!(pf.decision, Decision::NeedsConsent);
103        let dec = decide(&compiled, &Consent { accepted: true }, &gctx).unwrap();
104        assert_eq!(dec.decision, Decision::Allow);
105        assert!(dec.events.len() >= 2);
106    }
107}