solti_model/lib.rs
1//! # solti-model
2//!
3//! Shared resource model for Solti agents and control planes.
4//!
5//! This crate defines task resources, workloads, policies, selectors, queries,
6//! output events, capabilities, and bearer tokens.
7//! It does not execute tasks or own resource storage.
8//!
9//! ## Start Here
10//!
11//! Use [`TaskManifest`] for caller-owned desired state.
12//! Use [`Task`] for a stored resource with server metadata and status.
13//! Use [`TaskSpec`] to describe execution.
14//! Use [`TaskWorkload`] to select a built-in or extension workload.
15//!
16//! ## Resource Flow
17//!
18//! ```text
19//! caller
20//! │ TaskManifest
21//! ▼
22//! Task::from_manifest ── generates uid and creationTimestamp
23//! │ └─ starts generation at 1
24//! ▼
25//! Task
26//! ├── metadata: ObjectMeta
27//! ├── spec: TaskSpec
28//! ├── status: TaskStatus
29//! │
30//! └── state store assigns resourceVersion
31//! ```
32//!
33//! The model validates values and applies state transitions.
34//! Storage, reconciliation, execution, and transport stay in higher layers.
35//!
36//! ## Features
37//!
38//! The default `schema` feature implements `schemars::JsonSchema` for resource, workload, selector, capability, and output types.
39//! Disable default features when schema generation is not needed.
40//! Runtime validation remains authoritative for cross-field and byte-budget rules.
41//!
42//! ## Quick Start
43//!
44//! Build a task spec:
45//!
46//! ```rust
47//! use solti_model::{
48//! Flag, SubprocessMode, SubprocessSpec, TaskEnv, TaskSpec, TaskWorkload,
49//! };
50//!
51//! let workload = TaskWorkload::Subprocess(SubprocessSpec::new(
52//! SubprocessMode::Command {
53//! command: "echo".into(),
54//! args: vec!["hello".into()],
55//! },
56//! TaskEnv::default(),
57//! None,
58//! Flag::enabled(),
59//! ));
60//!
61//! let spec = TaskSpec::builder("hello", workload, 5_000u64)
62//! .build()
63//! .expect("valid spec");
64//!
65//! spec.validate().expect("valid spec");
66//! assert_eq!(spec.slot().as_str(), "hello");
67//! ```
68//!
69//! Create a stored task resource:
70//!
71//! ```rust
72//! use solti_model::{EmbeddedSpec, Task, TaskPhase, TaskSpec, TaskWorkload};
73//!
74//! let workload = TaskWorkload::Embedded(EmbeddedSpec::new("v1").unwrap());
75//! let spec = TaskSpec::builder("cleanup", workload, 1_000u64)
76//! .build()
77//! .unwrap();
78//!
79//! let task = Task::new("embedded-cleanup-1", spec).unwrap();
80//!
81//! assert_eq!(*task.phase(), TaskPhase::Pending);
82//! assert_eq!(task.name().as_str(), "embedded-cleanup-1");
83//! ```
84//!
85//! [`TaskWorkload::Embedded`] is valid in the shared model.
86//! API and runner layers apply their own admission rules.
87//!
88//! ## Resource Model
89//!
90//! ```text
91//! Task
92//! apiVersion, kind
93//! metadata: ObjectMeta
94//! spec: TaskSpec
95//! status: TaskStatus
96//!
97//! TaskSpec
98//! slot, workload, timeout, restart, backoff, admission
99//! max_retries, runner_selector
100//!
101//! TaskStatus
102//! observed_generation, conditions, phase, attempt, exit_code, error
103//! ```
104//!
105//! [`TaskSpec`] is desired state.
106//! [`TaskStatus`] is observed state.
107//! [`ObjectMeta`] carries identity, versions, labels, annotations, and timestamps.
108//!
109//! ## Lifecycle
110//!
111//! ```text
112//! Pending ──▶ Running ──▶ Succeeded
113//! ├────────▶ Failed
114//! ├────────▶ Timeout
115//! └────────▶ Canceled
116//!
117//! Failed | Timeout ── retry budget exhausted ──▶ Exhausted
118//! ```
119//!
120//! Terminal phases are `Succeeded`, `Failed`, `Timeout`, `Canceled`, and `Exhausted`.
121//! See [`TaskPhase::is_terminal`].
122//!
123//! ## Task Workloads
124//!
125//! [`TaskWorkload`] describes what a task runs:
126//!
127//! | Kind | Meaning | Routed by runner |
128//! |--------------|------------------------|------------------|
129//! | `Subprocess` | Host command or script | yes |
130//! | `Container` | OCI image | yes |
131//! | `Wasm` | WASI module | yes |
132//! | `Embedded` | In-process task | no |
133//! | `Extension` | Application-defined | yes |
134//!
135//! Routable variants are consumed by `solti-runner`.
136//! Embedded workloads bypass runner routing.
137//!
138//! ## Selectors
139//!
140//! [`LabelSelector`] matches runner labels. All requirements are ANDed:
141//!
142//! ```rust
143//! use solti_model::{Labels, LabelSelector, SelectorRequirement};
144//!
145//! let selector = LabelSelector {
146//! match_labels: {
147//! let mut labels = Labels::new();
148//! labels.insert("zone", "eu");
149//! labels
150//! },
151//! match_expressions: vec![SelectorRequirement::exists("gpu")],
152//! };
153//!
154//! let mut runner = Labels::new();
155//! runner.insert("zone", "eu");
156//! runner.insert("gpu", "h100");
157//!
158//! assert!(selector.matches(&runner));
159//! ```
160//!
161//! ## Auth
162//!
163//! [`Token`] wraps a bearer secret.
164//! Its `Debug` output is redacted.
165//! [`Token::verify`] uses a constant-time comparison for equal-length values.
166//!
167//! ## Main Types
168//!
169//! | Area | Types |
170//! |--------------|------------------------------------------------------------------------------------------------|
171//! | Resource | [`Task`], [`TaskManifest`], [`TaskSpec`], [`TaskStatus`], [`ObjectMeta`], [`TaskRun`] |
172//! | Identity | [`Slot`], [`TaskId`], [`AgentId`], [`Uid`] |
173//! | Workload | [`TaskWorkload`], [`ExtensionWorkload`], [`SubprocessSpec`], [`WasmSpec`], [`ContainerSpec`] |
174//! | Policies | [`RestartPolicy`], [`BackoffPolicy`], [`JitterPolicy`], [`AdmissionPolicy`], [`Timeout`] |
175//! | Selection | [`Labels`], [`LabelSelector`], [`SelectorRequirement`], [`SelectorOperator`] |
176//! | Capabilities | [`AgentCapabilities`], [`RunnerCapability`], [`WorkloadTypeMeta`] |
177//! | Query | [`TaskContinuation`], [`TaskFilter`], [`TaskQuery`], [`TaskPage`], [`TaskWatchEvent`] |
178//! | Output | [`OutputEvent`], [`OutputChunk`], [`StreamKind`] |
179//! | Auth | [`Token`] |
180//! | Errors | [`ModelError`], [`ModelResult`] |
181//!
182//! ## See Also
183//!
184//! - `solti-runner` consumes [`TaskSpec`] and [`TaskWorkload`] to build executable tasks.
185//! - `solti-core` manages [`Task`] lifecycle and state transitions.
186//! - `solti-api` serializes model types over gRPC and HTTP.
187
188#![forbid(unsafe_code)]
189#![warn(missing_docs)]
190
191/// Compiles the runnable Rust code blocks in `README.md` as doctests.
192#[cfg(doctest)]
193#[doc = include_str!("../README.md")]
194struct ReadmeDoctests;
195
196mod domain;
197pub use domain::{
198 AGENT_ID_MAX_LEN, AdmissionPolicy, AgentCapabilities, AgentId, BackoffPolicy, ContainerSpec,
199 DEFAULT_LIMIT, EmbeddedSpec, ExtensionWorkload, Flag, JitterPolicy, KeyValue, LabelSelector,
200 Labels, LabelsIter, MAX_LIMIT, MAX_SCRIPT_BODY_BYTES, OutputChunk, OutputEvent, RestartPolicy,
201 RunnerCapability, SLOT_MAX_LEN, SelectorOperator, SelectorRequirement, Slot, StreamKind,
202 SubprocessMode, SubprocessSpec, TASK_ID_MAX_LEN, TaskContinuation, TaskEnv, TaskFilter, TaskId,
203 TaskPage, TaskPhase, TaskQuery, TaskWatchEvent, TaskWorkload, Timeout, WORKLOAD_API_VERSION,
204 WasmSpec, WorkloadTypeMeta,
205};
206
207mod resource;
208pub use resource::{
209 Annotations, ConditionStatus, DesiredChange, ObjectMeta, TASK_API_VERSION,
210 TASK_API_VERSION_MAJOR, TASK_KIND, Task, TaskCondition, TaskConditionType, TaskManifest,
211 TaskManifestMeta, TaskRun, TaskSpec, TaskSpecBuilder, TaskStatus, TypeMeta, Uid,
212 WritePreconditions,
213};
214
215mod error;
216pub use error::{ModelError, ModelResult};
217
218mod auth;
219pub use auth::Token;
220
221mod validation;
222
223#[cfg(feature = "schema")]
224mod schema;