a3s_runtime/contract/
mod.rs1mod artifact;
2mod capabilities;
3mod network;
4mod observation;
5mod process;
6mod protocol;
7mod resource;
8mod unit;
9
10pub use artifact::{ArtifactRef, RuntimeOutputArtifact};
11pub use capabilities::{ResourceControl, RuntimeCapabilities, RuntimeFeature};
12pub use network::{NetworkMode, RuntimeNetworkSpec, RuntimePort, TransportProtocol};
13pub use observation::{
14 RuntimeEvidence, RuntimeFailure, RuntimeHealthObservation, RuntimeHealthState,
15 RuntimeInspection, RuntimeObservation, RuntimeUnitState, RuntimeUsage,
16};
17pub use process::{RuntimeProcessSpec, SecretReference, SecretTarget};
18pub use protocol::{
19 RuntimeActionRequest, RuntimeApplyRequest, RuntimeExecRequest, RuntimeExecResult,
20 RuntimeLogChunk, RuntimeLogQuery, RuntimeLogStream, RuntimeRemoval,
21};
22pub use resource::{IsolationLevel, ResourceLimits};
23pub use unit::{
24 HealthCheckKind, HealthProbe, MountKind, RestartPolicy, RuntimeHealthCheck, RuntimeMount,
25 RuntimeMountSource, RuntimeOutputSpec, RuntimeUnitClass, RuntimeUnitSpec,
26};
27
28pub(crate) fn validate_digest(value: &str) -> Result<(), String> {
29 let Some(hex) = value.strip_prefix("sha256:") else {
30 return Err("digest must use sha256".into());
31 };
32 if hex.len() != 64 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
33 return Err("digest must contain exactly 64 hexadecimal characters".into());
34 }
35 Ok(())
36}
37
38pub(crate) fn validate_nonempty(label: &str, value: &str, max: usize) -> Result<(), String> {
39 if value.is_empty() || value.len() > max || value.contains('\0') || value.contains(['\r', '\n'])
40 {
41 return Err(format!(
42 "{label} must be a bounded nonempty single-line value"
43 ));
44 }
45 Ok(())
46}
47
48pub(crate) fn validate_id(label: &str, value: &str, max: usize) -> Result<(), String> {
49 validate_nonempty(label, value, max)?;
50 if value
51 .bytes()
52 .any(|byte| !(byte.is_ascii_alphanumeric() || b"-_.:/".contains(&byte)))
53 {
54 return Err(format!("{label} contains unsupported characters"));
55 }
56 Ok(())
57}
58
59pub(crate) fn validate_name(label: &str, value: &str) -> Result<(), String> {
60 validate_nonempty(label, value, 255)?;
61 if value
62 .bytes()
63 .any(|byte| !(byte.is_ascii_alphanumeric() || b"-_ .".contains(&byte)))
64 || value.starts_with(['-', '_', '.', ' '])
65 || value.ends_with(['-', '_', '.', ' '])
66 {
67 return Err(format!("{label} contains unsupported characters"));
68 }
69 Ok(())
70}
71
72pub(crate) fn validate_absolute_path(label: &str, value: &str) -> Result<(), String> {
73 if !value.starts_with('/')
74 || value.len() > 4096
75 || value.contains('\0')
76 || value.split('/').any(|segment| segment == "..")
77 {
78 return Err(format!(
79 "{label} must be a bounded absolute path without '..'"
80 ));
81 }
82 Ok(())
83}
84
85pub(crate) fn validate_uri(label: &str, value: &str) -> Result<(), String> {
86 validate_nonempty(label, value, 4096)?;
87 let Some((scheme, rest)) = value.split_once("://") else {
88 return Err(format!("{label} must contain a URI scheme"));
89 };
90 if scheme.is_empty()
91 || rest.is_empty()
92 || !scheme
93 .bytes()
94 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
95 {
96 return Err(format!("{label} is invalid"));
97 }
98 Ok(())
99}