Skip to main content

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;