Skip to main content

agentics_contracts/validation/
targets.rs

1//! Shared target selection and MVP target policy validation.
2
3use 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
8/// Hosted MVP target with no accelerator.
9pub const LINUX_ARM64_NO_ACCELERATOR_TARGET: &str = "linux-arm64-cpu";
10/// Hosted MVP target with CUDA-capable accelerator access.
11pub const LINUX_ARM64_ACCELERATOR_TARGET: &str = "linux-arm64-cuda";
12/// Local process-rehearsal target for platform development only.
13pub const MACOS_ARM64_NO_ACCELERATOR_DEV_TARGET: &str = "macos-arm64-cpu";
14
15/// Target selection mode for submit and validate workflows.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TargetSelectionMode {
18    Official,
19    Validation,
20}
21
22/// Select target names from a challenge target list using the shared CLI/API contract.
23pub 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
66/// Validate that selected targets can be used for the requested workflow.
67fn 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
91/// Validate one challenge target against the hosted MVP target policy.
92pub 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
120/// Validate a local platform-development target name.
121pub 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
135/// Require the platform and accelerator fields that a target name implies.
136fn 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
159/// Render accelerator values in the public JSON notation.
160fn 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}