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
8#[cfg(feature = "dv25")]
9use logline_core as _;
10
11use serde::{Deserialize, Serialize};
12use tdln_compiler::CompiledIntent;
13use thiserror::Error;
14
15#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
16pub enum Decision {
17    Allow,
18    Deny,
19    NeedsConsent,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct LogEvent {
24    pub kind: String,
25    pub payload: serde_json::Value,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct GateOutput {
30    pub decision: Decision,
31    pub audit: serde_json::Value,
32    pub proof_ref: [u8; 32],
33    pub events: Vec<LogEvent>,
34}
35
36#[derive(Debug, Error)]
37pub enum GateError {
38    #[error("invalid input")]
39    Invalid,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PolicyCtx {
44    pub allow_freeform: bool,
45}
46
47/// Pré-validação determinística.
48///
49/// # Errors
50///
51/// - Retorna `GateError::Invalid` para entradas inválidas (reservado)
52pub fn preflight(intent: &CompiledIntent, _ctx: &PolicyCtx) -> Result<GateOutput, GateError> {
53    // Minimal deterministic audit
54    let audit = serde_json::json!({
55        "ast_cid": hex::encode(intent.proof.ast_cid),
56        "canon_cid": hex::encode(intent.proof.canon_cid),
57    });
58    let events = vec![LogEvent {
59        kind: "policy.preflight".into(),
60        payload: audit.clone(),
61    }];
62    Ok(GateOutput {
63        decision: Decision::NeedsConsent,
64        audit,
65        proof_ref: intent.cid,
66        events,
67    })
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Consent {
72    pub accepted: bool,
73}
74
75/// Decide a partir do consentimento e contexto.
76///
77/// # Errors
78///
79/// - Propaga `GateError::Invalid` para entradas inválidas (reservado)
80pub fn decide(
81    intent: &CompiledIntent,
82    consent: &Consent,
83    ctx: &PolicyCtx,
84) -> Result<GateOutput, GateError> {
85    // Deterministic decision with audit trail
86    let mut out = preflight(intent, ctx)?;
87    let decision = if consent.accepted && ctx.allow_freeform {
88        Decision::Allow
89    } else if !consent.accepted {
90        Decision::NeedsConsent
91    } else {
92        Decision::Deny
93    };
94    out.events.push(LogEvent {
95        kind: "policy.decision".into(),
96        payload: serde_json::json!({ "decision": format!("{:?}", decision) }),
97    });
98    out.decision = decision;
99    Ok(out)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use tdln_compiler::{compile, CompileCtx};
106    #[test]
107    fn pipeline() {
108        let ctx = CompileCtx {
109            rule_set: "v1".into(),
110        };
111        let compiled = compile("hello world", &ctx).unwrap();
112        let gctx = PolicyCtx {
113            allow_freeform: true,
114        };
115        let pf = preflight(&compiled, &gctx).unwrap();
116        assert_eq!(pf.decision, Decision::NeedsConsent);
117        let dec = decide(&compiled, &Consent { accepted: true }, &gctx).unwrap();
118        assert_eq!(dec.decision, Decision::Allow);
119        assert!(dec.events.len() >= 2);
120    }
121}