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::HashSet;
83
84 use super::*;
85 use crate::model::context::{AgentContext, FileRange, LoadFile};
86 use crate::model::fields::{NodeType, Span};
87 use crate::model::node::Node;
88
89 fn minimal_node() -> Node {
90 Node {
91 id: "test.node".to_owned(),
92 node_type: NodeType::Facts,
93 summary: "a test node".to_owned(),
94 span: Span::new(5, 7),
95 ..Default::default()
96 }
97 }
98
99 #[test]
100 fn test_validate_context_none_returns_empty() {
101 let node = minimal_node();
102 let all_ids = HashSet::new();
103 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
104 assert!(errors.is_empty());
105 }
106
107 #[test]
108 fn test_validate_context_load_nodes_valid_returns_empty() {
109 let mut node = minimal_node();
110 node.agent_context = Some(AgentContext {
111 load_nodes: Some(vec!["auth.login".to_owned()]),
112 load_files: None,
113 system_hint: None,
114 max_tokens: None,
115 load_memory: None,
116 });
117 let mut all_ids = HashSet::new();
118 all_ids.insert("auth.login".to_owned());
119 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
120 assert!(errors.is_empty());
121 }
122
123 #[test]
124 fn test_validate_context_load_nodes_unresolved_returns_v004() {
125 let mut node = minimal_node();
126 node.agent_context = Some(AgentContext {
127 load_nodes: Some(vec!["missing.node".to_owned()]),
128 load_files: None,
129 system_hint: None,
130 max_tokens: None,
131 load_memory: None,
132 });
133 let all_ids = HashSet::new();
134 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
135 assert!(errors.iter().any(|e| e.code == ErrorCode::V004));
136 }
137
138 #[test]
139 fn test_validate_context_load_files_relative_path_returns_empty() {
140 let mut node = minimal_node();
141 node.agent_context = Some(AgentContext {
142 load_nodes: None,
143 load_files: Some(vec![LoadFile {
144 path: "src/auth.rs".to_owned(),
145 range: FileRange::Full,
146 }]),
147 system_hint: None,
148 max_tokens: None,
149 load_memory: None,
150 });
151 let all_ids = HashSet::new();
152 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
153 assert!(errors.is_empty());
154 }
155
156 #[test]
157 fn test_validate_context_load_files_absolute_returns_v015() {
158 let mut node = minimal_node();
159 node.agent_context = Some(AgentContext {
160 load_nodes: None,
161 load_files: Some(vec![LoadFile {
162 path: "/etc/passwd".to_owned(),
163 range: FileRange::Full,
164 }]),
165 system_hint: None,
166 max_tokens: None,
167 load_memory: None,
168 });
169 let all_ids = HashSet::new();
170 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
171 assert!(errors.iter().any(|e| e.code == ErrorCode::V015));
172 }
173
174 #[test]
175 fn test_validate_context_load_files_traversal_returns_v015() {
176 let mut node = minimal_node();
177 node.agent_context = Some(AgentContext {
178 load_nodes: None,
179 load_files: Some(vec![LoadFile {
180 path: "src/../../../etc/shadow".to_owned(),
181 range: FileRange::Full,
182 }]),
183 system_hint: None,
184 max_tokens: None,
185 load_memory: None,
186 });
187 let all_ids = HashSet::new();
188 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
189 assert!(errors.iter().any(|e| e.code == ErrorCode::V015));
190 }
191
192 #[test]
193 fn test_validate_context_load_memory_none_returns_empty() {
194 let node = minimal_node();
195 let all_ids = HashSet::new();
196 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
197 assert!(errors.is_empty());
198 }
199
200 #[test]
201 fn test_validate_context_load_memory_valid_returns_empty() {
202 let mut node = minimal_node();
203 node.agent_context = Some(AgentContext {
204 load_nodes: None,
205 load_files: None,
206 system_hint: None,
207 max_tokens: None,
208 load_memory: Some(vec!["rust.repository".to_owned()]),
209 });
210 let all_ids = HashSet::new();
211 let mut all_memory_topics = HashSet::new();
212 all_memory_topics.insert("rust.repository".to_owned());
213 let errors = validate_context(&node, &all_ids, &all_memory_topics, "test.agm");
214 assert!(errors.is_empty());
215 }
216
217 #[test]
218 fn test_validate_context_load_memory_unresolved_returns_v026() {
219 let mut node = minimal_node();
220 node.agent_context = Some(AgentContext {
221 load_nodes: None,
222 load_files: None,
223 system_hint: None,
224 max_tokens: None,
225 load_memory: Some(vec!["rust.repository".to_owned()]),
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::V026));
230 }
231
232 #[test]
233 fn test_validate_context_load_memory_invalid_format_returns_v025() {
234 let mut node = minimal_node();
235 node.agent_context = Some(AgentContext {
236 load_nodes: None,
237 load_files: None,
238 system_hint: None,
239 max_tokens: None,
240 load_memory: Some(vec!["Rust.Models".to_owned()]),
241 });
242 let all_ids = HashSet::new();
243 let errors = validate_context(&node, &all_ids, &HashSet::new(), "test.agm");
244 assert!(errors.iter().any(|e| e.code == ErrorCode::V025));
245 }
246}