1use std::collections::HashSet;
6
7use crate::error::codes::ErrorCode;
8use crate::error::diagnostic::{AgmError, ErrorLocation, Severity};
9use crate::model::node::Node;
10
11fn is_unsafe_path(path: &str) -> bool {
13 path.starts_with('/') || path.starts_with('\\') || path.contains("..")
14}
15
16#[must_use]
21pub fn validate_context(
22 node: &Node,
23 all_ids: &HashSet<String>,
24 all_memory_topics: &HashSet<String>,
25 file_name: &str,
26) -> Vec<AgmError> {
27 let ctx = match &node.agent_context {
28 Some(c) => c,
29 None => return Vec::new(),
30 };
31
32 let mut errors = Vec::new();
33 let line = node.span.start_line;
34 let id = node.id.as_str();
35
36 if let Some(ref load_nodes) = ctx.load_nodes {
38 for ref_id in load_nodes {
39 if !all_ids.contains(ref_id.as_str()) {
40 errors.push(AgmError::new(
41 ErrorCode::V004,
42 format!(
43 "Unresolved reference `{ref_id}` in `agent_context.load_nodes` of node `{id}`"
44 ),
45 ErrorLocation::full(file_name, line, id),
46 ));
47 }
48 }
49 }
50
51 if let Some(ref load_files) = ctx.load_files {
53 for load_file in load_files {
54 if is_unsafe_path(&load_file.path) {
55 errors.push(AgmError::with_severity(
56 ErrorCode::V015,
57 Severity::Warning,
58 format!(
59 "`target` path is absolute or contains traversal: `{}`",
60 load_file.path
61 ),
62 ErrorLocation::full(file_name, line, id),
63 ));
64 }
65 }
66 }
67
68 if let Some(ref load_memory) = ctx.load_memory {
70 errors.extend(crate::memory::schema::validate_load_memory(
71 load_memory,
72 all_memory_topics,
73 ErrorLocation::full(file_name, line, id),
74 ));
75 }
76
77 errors
78}
79
80#[cfg(test)]
81mod tests {
82 use std::collections::BTreeMap;
83 use std::collections::HashSet;
84
85 use super::*;
86 use crate::model::context::{AgentContext, FileRange, LoadFile};
87 use crate::model::fields::{NodeType, Span};
88 use crate::model::node::Node;
89
90 fn minimal_node() -> Node {
91 Node {
92 id: "test.node".to_owned(),
93 node_type: NodeType::Facts,
94 summary: "a test node".to_owned(),
95 priority: None,
96 stability: None,
97 confidence: None,
98 status: None,
99 depends: None,
100 related_to: None,
101 replaces: None,
102 conflicts: None,
103 see_also: None,
104 items: None,
105 steps: None,
106 fields: None,
107 input: None,
108 output: None,
109 detail: None,
110 rationale: None,
111 tradeoffs: None,
112 resolution: None,
113 examples: None,
114 notes: None,
115 code: None,
116 code_blocks: None,
117 verify: None,
118 agent_context: None,
119 target: None,
120 execution_status: None,
121 executed_by: None,
122 executed_at: None,
123 execution_log: None,
124 retry_count: None,
125 parallel_groups: None,
126 memory: None,
127 scope: None,
128 applies_when: None,
129 valid_from: None,
130 valid_until: None,
131 tags: None,
132 aliases: None,
133 keywords: None,
134 extra_fields: BTreeMap::new(),
135 span: Span::new(5, 7),
136 }
137 }
138
139 #[test]
140 fn test_validate_context_none_returns_empty() {
141 let node = minimal_node();
142 let all_ids = HashSet::new();
143 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
144 assert!(errors.is_empty());
145 }
146
147 #[test]
148 fn test_validate_context_load_nodes_valid_returns_empty() {
149 let mut node = minimal_node();
150 node.agent_context = Some(AgentContext {
151 load_nodes: Some(vec!["auth.login".to_owned()]),
152 load_files: None,
153 system_hint: None,
154 max_tokens: None,
155 load_memory: None,
156 });
157 let mut all_ids = HashSet::new();
158 all_ids.insert("auth.login".to_owned());
159 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
160 assert!(errors.is_empty());
161 }
162
163 #[test]
164 fn test_validate_context_load_nodes_unresolved_returns_v004() {
165 let mut node = minimal_node();
166 node.agent_context = Some(AgentContext {
167 load_nodes: Some(vec!["missing.node".to_owned()]),
168 load_files: None,
169 system_hint: None,
170 max_tokens: None,
171 load_memory: None,
172 });
173 let all_ids = HashSet::new();
174 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
175 assert!(errors.iter().any(|e| e.code == ErrorCode::V004));
176 }
177
178 #[test]
179 fn test_validate_context_load_files_relative_path_returns_empty() {
180 let mut node = minimal_node();
181 node.agent_context = Some(AgentContext {
182 load_nodes: None,
183 load_files: Some(vec![LoadFile {
184 path: "src/auth.rs".to_owned(),
185 range: FileRange::Full,
186 }]),
187 system_hint: None,
188 max_tokens: None,
189 load_memory: None,
190 });
191 let all_ids = HashSet::new();
192 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
193 assert!(errors.is_empty());
194 }
195
196 #[test]
197 fn test_validate_context_load_files_absolute_returns_v015() {
198 let mut node = minimal_node();
199 node.agent_context = Some(AgentContext {
200 load_nodes: None,
201 load_files: Some(vec![LoadFile {
202 path: "/etc/passwd".to_owned(),
203 range: FileRange::Full,
204 }]),
205 system_hint: None,
206 max_tokens: None,
207 load_memory: None,
208 });
209 let all_ids = HashSet::new();
210 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
211 assert!(errors.iter().any(|e| e.code == ErrorCode::V015));
212 }
213
214 #[test]
215 fn test_validate_context_load_files_traversal_returns_v015() {
216 let mut node = minimal_node();
217 node.agent_context = Some(AgentContext {
218 load_nodes: None,
219 load_files: Some(vec![LoadFile {
220 path: "src/../../../etc/shadow".to_owned(),
221 range: FileRange::Full,
222 }]),
223 system_hint: None,
224 max_tokens: None,
225 load_memory: None,
226 });
227 let all_ids = HashSet::new();
228 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
229 assert!(errors.iter().any(|e| e.code == ErrorCode::V015));
230 }
231
232 #[test]
233 fn test_validate_context_load_memory_none_returns_empty() {
234 let node = minimal_node();
235 let all_ids = HashSet::new();
236 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
237 assert!(errors.is_empty());
238 }
239
240 #[test]
241 fn test_validate_context_load_memory_valid_returns_empty() {
242 let mut node = minimal_node();
243 node.agent_context = Some(AgentContext {
244 load_nodes: None,
245 load_files: None,
246 system_hint: None,
247 max_tokens: None,
248 load_memory: Some(vec!["rust.repository".to_owned()]),
249 });
250 let all_ids = HashSet::new();
251 let mut all_memory_topics = HashSet::new();
252 all_memory_topics.insert("rust.repository".to_owned());
253 let errors = validate_context(&node, &all_ids, &all_memory_topics, "test.agm");
254 assert!(errors.is_empty());
255 }
256
257 #[test]
258 fn test_validate_context_load_memory_unresolved_returns_v026() {
259 let mut node = minimal_node();
260 node.agent_context = Some(AgentContext {
261 load_nodes: None,
262 load_files: None,
263 system_hint: None,
264 max_tokens: None,
265 load_memory: Some(vec!["rust.repository".to_owned()]),
266 });
267 let all_ids = HashSet::new();
268 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
269 assert!(errors.iter().any(|e| e.code == ErrorCode::V026));
270 }
271
272 #[test]
273 fn test_validate_context_load_memory_invalid_format_returns_v025() {
274 let mut node = minimal_node();
275 node.agent_context = Some(AgentContext {
276 load_nodes: None,
277 load_files: None,
278 system_hint: None,
279 max_tokens: None,
280 load_memory: Some(vec!["Rust.Models".to_owned()]),
281 });
282 let all_ids = HashSet::new();
283 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
284 assert!(errors.iter().any(|e| e.code == ErrorCode::V025));
285 }
286}