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