agentics_contracts/validation/
targets.rs1use crate::validation::archive::ChallengeValidationError;
4use agentics_domain::models::challenge::{ChallengeTargetSpec, DockerPlatform, TargetAccelerator};
5use agentics_domain::models::names::{ChallengeName, TargetName};
6use agentics_error::{Result, ServiceError};
7
8pub const LINUX_ARM64_NO_ACCELERATOR_TARGET: &str = "linux-arm64-cpu";
10pub const LINUX_ARM64_ACCELERATOR_TARGET: &str = "linux-arm64-cuda";
12pub const MACOS_ARM64_NO_ACCELERATOR_DEV_TARGET: &str = "macos-arm64-cpu";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TargetSelectionMode {
18 Official,
19 Validation,
20}
21
22pub fn select_targets_from_spec(
24 challenge_name: &ChallengeName,
25 targets: &[ChallengeTargetSpec],
26 requested_target: Option<&TargetName>,
27 all_targets: bool,
28 mode: TargetSelectionMode,
29) -> Result<Vec<TargetName>> {
30 if all_targets {
31 let selected = targets.iter().collect::<Vec<_>>();
32 validate_selected_targets(challenge_name, &selected, mode)?;
33 return Ok(selected.iter().map(|target| target.name.clone()).collect());
34 }
35
36 if let Some(target) = requested_target {
37 let target = targets
38 .iter()
39 .find(|candidate| &candidate.name == target)
40 .ok_or_else(|| {
41 ServiceError::from(ChallengeValidationError::UnsupportedTarget(format!(
42 "challenge `{challenge_name}` does not support target `{target}`"
43 )))
44 })?;
45 validate_selected_targets(challenge_name, &[target], mode)?;
46 return Ok(vec![target.name.clone()]);
47 }
48
49 match targets {
50 [] => Err(ServiceError::Validation(format!(
51 "challenge `{challenge_name}` does not declare any targets"
52 ))),
53 targets => {
54 let available = targets
55 .iter()
56 .map(|target| target.name.as_str())
57 .collect::<Vec<_>>()
58 .join(", ");
59 Err(ServiceError::Validation(format!(
60 "target is required for challenge `{challenge_name}`; pass --target <target> or --all-targets. Available targets: {available}"
61 )))
62 }
63 }
64}
65
66fn validate_selected_targets(
68 challenge_name: &ChallengeName,
69 targets: &[&ChallengeTargetSpec],
70 mode: TargetSelectionMode,
71) -> Result<()> {
72 if mode != TargetSelectionMode::Validation {
73 return Ok(());
74 }
75
76 let disabled = targets
77 .iter()
78 .filter(|target| !target.validation_enabled)
79 .map(|target| target.name.as_str())
80 .collect::<Vec<_>>();
81 if disabled.is_empty() {
82 return Ok(());
83 }
84
85 Err(ServiceError::Validation(format!(
86 "validation pass is disabled for challenge `{challenge_name}` target(s): {}; submit officially or ask the challenge owner to enable validation",
87 disabled.join(", ")
88 )))
89}
90
91pub fn validate_submission_target_policy(target: &ChallengeTargetSpec, field: &str) -> Result<()> {
93 match target.name.as_str() {
94 LINUX_ARM64_NO_ACCELERATOR_TARGET => require_target_shape(
95 target,
96 field,
97 DockerPlatform::LinuxArm64,
98 TargetAccelerator::None,
99 ),
100 LINUX_ARM64_ACCELERATOR_TARGET => require_target_shape(
101 target,
102 field,
103 DockerPlatform::LinuxArm64,
104 TargetAccelerator::Gpu,
105 ),
106 MACOS_ARM64_NO_ACCELERATOR_DEV_TARGET => Err(ServiceError::Validation(format!(
107 "{field}.name `{}` is a platform-development target and cannot be used for hosted challenge deployment or submissions",
108 target.name
109 ))),
110 "linux-amd64-cpu" | "linux-amd64-cuda" => Err(ServiceError::Validation(format!(
111 "{field}.name `{}` is reserved for post-MVP deployment support",
112 target.name
113 ))),
114 other => Err(ServiceError::Validation(format!(
115 "{field}.name `{other}` is not supported for MVP hosted challenge deployment; supported targets: {LINUX_ARM64_NO_ACCELERATOR_TARGET}, {LINUX_ARM64_ACCELERATOR_TARGET}"
116 ))),
117 }
118}
119
120pub fn validate_platform_dev_target_name(target: &TargetName, field: &str) -> Result<()> {
122 match target.as_str() {
123 LINUX_ARM64_NO_ACCELERATOR_TARGET
124 | LINUX_ARM64_ACCELERATOR_TARGET
125 | MACOS_ARM64_NO_ACCELERATOR_DEV_TARGET => Ok(()),
126 "linux-amd64-cpu" | "linux-amd64-cuda" => Err(ServiceError::Validation(format!(
127 "{field} `{target}` is reserved for post-MVP platform development"
128 ))),
129 other => Err(ServiceError::Validation(format!(
130 "{field} `{other}` is not supported for MVP platform development; supported targets: {LINUX_ARM64_NO_ACCELERATOR_TARGET}, {LINUX_ARM64_ACCELERATOR_TARGET}, {MACOS_ARM64_NO_ACCELERATOR_DEV_TARGET}"
131 ))),
132 }
133}
134
135fn require_target_shape(
137 target: &ChallengeTargetSpec,
138 field: &str,
139 docker_platform: DockerPlatform,
140 accelerator: TargetAccelerator,
141) -> Result<()> {
142 if target.docker_platform != docker_platform {
143 return Err(ServiceError::Validation(format!(
144 "{field}.docker_platform must be `{}` for target `{}`",
145 docker_platform.as_str(),
146 target.name
147 )));
148 }
149 if target.accelerator != accelerator {
150 return Err(ServiceError::Validation(format!(
151 "{field}.accelerator must be {} for target `{}`",
152 accelerator_json_name(accelerator),
153 target.name
154 )));
155 }
156 Ok(())
157}
158
159fn accelerator_json_name(accelerator: TargetAccelerator) -> &'static str {
161 match accelerator {
162 TargetAccelerator::None => "null",
163 TargetAccelerator::Gpu => "\"gpu\"",
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use crate::zip_project::ZipProjectNetworkAccess;
170 use agentics_domain::models::challenge::{
171 ChallengeTargetSpec, DockerPlatform, EvaluatorStageProfiles, ResourceProfileSpec,
172 SolutionStageProfiles, StageResourceProfile, TargetAccelerator,
173 };
174 use agentics_domain::models::images::{ChallengeImageReference, LocalAgenticsImageReference};
175 use agentics_domain::models::names::{ChallengeName, ResourceProfileName, TargetName};
176
177 use super::{
178 LINUX_ARM64_ACCELERATOR_TARGET, LINUX_ARM64_NO_ACCELERATOR_TARGET, TargetSelectionMode,
179 select_targets_from_spec, validate_platform_dev_target_name,
180 validate_submission_target_policy,
181 };
182
183 fn challenge_name() -> ChallengeName {
184 ChallengeName::try_new("sample-sum".to_string()).expect("challenge name")
185 }
186
187 fn target_name(value: &str) -> TargetName {
188 TargetName::try_new(value.to_string()).expect("target name")
189 }
190
191 fn target(
192 value: &str,
193 accelerator: TargetAccelerator,
194 validation_enabled: bool,
195 ) -> ChallengeTargetSpec {
196 let image = ChallengeImageReference::Local {
197 reference: LocalAgenticsImageReference::try_new(
198 "agentics-linux-arm64-cpu:ubuntu26.04-local",
199 )
200 .expect("image"),
201 };
202 ChallengeTargetSpec {
203 name: target_name(value),
204 docker_platform: DockerPlatform::LinuxArm64,
205 accelerator,
206 validation_enabled,
207 resource_profile: ResourceProfileSpec {
208 name: ResourceProfileName::try_new("agentics-small".to_string()).expect("profile"),
209 resource_description: None,
210 solution_image: image.clone(),
211 evaluator_image: image,
212 solution: SolutionStageProfiles {
213 setup: stage_profile(30, 512, 1000, 1024),
214 build: stage_profile(30, 512, 1000, 1024),
215 run: Some(stage_profile(30, 512, 1000, 1024)),
216 },
217 evaluator: EvaluatorStageProfiles {
218 setup: stage_profile(30, 512, 1000, 1024),
219 run: stage_profile(30, 512, 1000, 1024),
220 },
221 hardware_metadata: None,
222 },
223 }
224 }
225
226 fn stage_profile(
227 timeout_sec: u64,
228 memory_limit_mb: u64,
229 cpu_limit_millis: u32,
230 disk_limit_mb: u64,
231 ) -> StageResourceProfile {
232 StageResourceProfile {
233 timeout_sec,
234 memory_limit_mb,
235 cpu_limit_millis,
236 disk_limit_mb,
237 network_access: ZipProjectNetworkAccess::Disabled,
238 }
239 }
240
241 #[test]
242 fn selects_targets_with_validation_policy() {
243 let challenge_name = challenge_name();
244 let targets = vec![
245 target(
246 LINUX_ARM64_NO_ACCELERATOR_TARGET,
247 TargetAccelerator::None,
248 true,
249 ),
250 target(
251 LINUX_ARM64_ACCELERATOR_TARGET,
252 TargetAccelerator::Gpu,
253 false,
254 ),
255 ];
256
257 let selected = select_targets_from_spec(
258 &challenge_name,
259 &targets,
260 Some(&target_name(LINUX_ARM64_NO_ACCELERATOR_TARGET)),
261 false,
262 TargetSelectionMode::Validation,
263 )
264 .expect("enabled target should select");
265 assert_eq!(
266 selected,
267 vec![target_name(LINUX_ARM64_NO_ACCELERATOR_TARGET)]
268 );
269
270 assert!(
271 select_targets_from_spec(
272 &challenge_name,
273 &targets,
274 None,
275 true,
276 TargetSelectionMode::Validation,
277 )
278 .is_err()
279 );
280 }
281
282 #[test]
283 fn validates_mvp_target_policy() {
284 let valid = target(
285 LINUX_ARM64_NO_ACCELERATOR_TARGET,
286 TargetAccelerator::None,
287 true,
288 );
289 validate_submission_target_policy(&valid, "targets[0]").expect("target should validate");
290
291 let invalid = target("main", TargetAccelerator::None, true);
292 assert!(validate_submission_target_policy(&invalid, "targets[0]").is_err());
293
294 let mismatched = target(
295 LINUX_ARM64_ACCELERATOR_TARGET,
296 TargetAccelerator::None,
297 true,
298 );
299 assert!(validate_submission_target_policy(&mismatched, "targets[0]").is_err());
300 }
301
302 #[test]
303 fn validates_platform_dev_targets() {
304 validate_platform_dev_target_name(&target_name("macos-arm64-cpu"), "target")
305 .expect("macos dev target should validate");
306 assert!(
307 validate_platform_dev_target_name(&target_name("linux-amd64-cpu"), "target").is_err()
308 );
309 }
310}