Skip to main content

tf_types/
tf_manifests.rs

1#![allow(clippy::field_reassign_with_default)]
2//! `.tf/` manifest loader — Rust mirror of
3//! `tools/tf-types-ts/src/core/tf-manifests.ts`.
4
5use std::collections::HashMap;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[derive(Clone, Debug, Default)]
13pub struct TfManifestPaths {
14    pub root_dir: PathBuf,
15    pub agent_contract: Option<PathBuf>,
16    pub threat_model: Option<PathBuf>,
17    pub policy: Option<PathBuf>,
18    pub actions: Option<PathBuf>,
19    pub proof_profile: Option<PathBuf>,
20    pub codegen: Option<PathBuf>,
21    pub conformance: Option<PathBuf>,
22}
23
24#[derive(Clone, Debug, Default, Serialize, Deserialize)]
25pub struct TfManifests {
26    pub agent_contract: Option<Value>,
27    pub threat_model: Option<Value>,
28    pub policy: Option<Value>,
29    pub actions: Option<Value>,
30    pub proof_profile: Option<Value>,
31    pub codegen: Option<HashMap<String, String>>,
32    pub conformance: Option<Value>,
33    pub diagnostics: Vec<TfDiagnostic>,
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
37pub struct TfDiagnostic {
38    pub file: String,
39    pub reason: String,
40}
41
42#[derive(Clone, Debug, Default)]
43pub struct TfFeatureGate {
44    pub policy: Option<Value>,
45    pub claimed_profiles: Vec<String>,
46    pub default_proof_level: Option<String>,
47    pub anchors: Vec<Value>,
48    pub forbidden_actions: Vec<String>,
49    pub per_action_proof_level: HashMap<String, String>,
50}
51
52impl TfFeatureGate {
53    pub fn proof_level_for_action(&self, action: &str) -> Option<&str> {
54        self.per_action_proof_level.get(action).map(String::as_str)
55    }
56}
57
58const REL_AGENT_CONTRACT: &str = ".tf/agent-contract.yaml";
59const REL_THREAT_MODEL: &str = ".tf/threat-model.yaml";
60const REL_POLICY: &str = ".tf/policy.yaml";
61const REL_ACTIONS: &str = ".tf/actions.yaml";
62const REL_PROOF_PROFILE: &str = ".tf/proof-profile.yaml";
63const REL_CODEGEN: &str = ".tf/codegen.toml";
64const REL_CONFORMANCE: &str = ".tf/conformance.json";
65
66pub fn load_tf_manifests(paths: &TfManifestPaths) -> TfManifests {
67    let mut out = TfManifests::default();
68    let try_yaml = |path: &Path, target: &mut Option<Value>, diags: &mut Vec<TfDiagnostic>| {
69        if !path.exists() {
70            return;
71        }
72        match fs::read_to_string(path)
73            .map_err(|e| e.to_string())
74            .and_then(|raw| crate::yaml::parse(&raw).map_err(|e| e.to_string()))
75        {
76            Ok(v) => *target = Some(v),
77            Err(reason) => diags.push(TfDiagnostic {
78                file: path.display().to_string(),
79                reason,
80            }),
81        }
82    };
83    let try_json = |path: &Path, target: &mut Option<Value>, diags: &mut Vec<TfDiagnostic>| {
84        if !path.exists() {
85            return;
86        }
87        match fs::read_to_string(path)
88            .map_err(|e| e.to_string())
89            .and_then(|raw| serde_json::from_str::<Value>(&raw).map_err(|e| e.to_string()))
90        {
91            Ok(v) => *target = Some(v),
92            Err(reason) => diags.push(TfDiagnostic {
93                file: path.display().to_string(),
94                reason,
95            }),
96        }
97    };
98
99    let agent = paths
100        .agent_contract
101        .clone()
102        .unwrap_or_else(|| paths.root_dir.join(REL_AGENT_CONTRACT));
103    try_yaml(&agent, &mut out.agent_contract, &mut out.diagnostics);
104    let tm = paths
105        .threat_model
106        .clone()
107        .unwrap_or_else(|| paths.root_dir.join(REL_THREAT_MODEL));
108    try_yaml(&tm, &mut out.threat_model, &mut out.diagnostics);
109    let policy = paths
110        .policy
111        .clone()
112        .unwrap_or_else(|| paths.root_dir.join(REL_POLICY));
113    try_yaml(&policy, &mut out.policy, &mut out.diagnostics);
114    let actions = paths
115        .actions
116        .clone()
117        .unwrap_or_else(|| paths.root_dir.join(REL_ACTIONS));
118    try_yaml(&actions, &mut out.actions, &mut out.diagnostics);
119    let pp = paths
120        .proof_profile
121        .clone()
122        .unwrap_or_else(|| paths.root_dir.join(REL_PROOF_PROFILE));
123    try_yaml(&pp, &mut out.proof_profile, &mut out.diagnostics);
124    let cf = paths
125        .conformance
126        .clone()
127        .unwrap_or_else(|| paths.root_dir.join(REL_CONFORMANCE));
128    try_json(&cf, &mut out.conformance, &mut out.diagnostics);
129
130    let cg = paths
131        .codegen
132        .clone()
133        .unwrap_or_else(|| paths.root_dir.join(REL_CODEGEN));
134    if cg.exists() {
135        match fs::read_to_string(&cg) {
136            Ok(raw) => out.codegen = Some(parse_tiny_toml(&raw)),
137            Err(e) => out.diagnostics.push(TfDiagnostic {
138                file: cg.display().to_string(),
139                reason: e.to_string(),
140            }),
141        }
142    }
143    out
144}
145
146pub fn build_feature_gate(manifests: &TfManifests) -> TfFeatureGate {
147    let mut gate = TfFeatureGate::default();
148    gate.policy = manifests.policy.clone();
149    if let Some(conf) = manifests.conformance.as_ref() {
150        if let Some(arr) = conf.get("claimed_profiles").and_then(|v| v.as_array()) {
151            gate.claimed_profiles = arr
152                .iter()
153                .filter_map(|v| v.as_str().map(str::to_string))
154                .collect();
155        }
156    }
157    if let Some(pp) = manifests.proof_profile.as_ref() {
158        gate.default_proof_level = pp
159            .get("default_proof_level")
160            .and_then(|v| v.as_str())
161            .map(str::to_string)
162            .or_else(|| {
163                pp.get("default_level")
164                    .and_then(|v| v.as_str())
165                    .map(str::to_string)
166            });
167        if let Some(actions) = pp.get("actions").and_then(|v| v.as_array()) {
168            for a in actions {
169                if let (Some(name), Some(level)) = (
170                    a.get("name").and_then(|v| v.as_str()),
171                    a.get("level").and_then(|v| v.as_str()),
172                ) {
173                    gate.per_action_proof_level
174                        .insert(name.to_string(), level.to_string());
175                }
176            }
177        }
178        if let Some(anchors) = pp.get("anchors").and_then(|v| v.as_array()) {
179            gate.anchors = anchors.clone();
180        }
181    }
182    if let Some(ac) = manifests.agent_contract.as_ref() {
183        if let Some(forbidden) = ac.get("forbidden").and_then(|v| v.as_array()) {
184            for f in forbidden {
185                if let Some(name) = f.get("action").and_then(|v| v.as_str()) {
186                    gate.forbidden_actions.push(name.to_string());
187                }
188            }
189        }
190    }
191    gate
192}
193
194fn parse_tiny_toml(raw: &str) -> HashMap<String, String> {
195    let mut out = HashMap::new();
196    for line in raw.lines() {
197        let stripped = line.split('#').next().unwrap_or(line).trim();
198        if stripped.is_empty() {
199            continue;
200        }
201        if let Some((k, v)) = stripped.split_once('=') {
202            let key = k.trim();
203            let mut value = v.trim();
204            if (value.starts_with('"') && value.ends_with('"'))
205                || (value.starts_with('\'') && value.ends_with('\''))
206            {
207                value = &value[1..value.len() - 1];
208            }
209            out.insert(key.to_string(), value.to_string());
210        }
211    }
212    out
213}