Skip to main content

clawft_kernel/workload_kind/
mod.rs

1//! Workload-kind registry (cog-boundary audit M3; ADR-099 section 2).
2//!
3//! A workload kind owns the pieces of the placement path that depend on
4//! what is being placed: manifest body validation, how a verified package
5//! becomes a [`WorkloadSpec`], and which adapter routes can run it. The
6//! rest of the path (signature checks, seeding, gating, scoring, dispatch)
7//! is kind-agnostic and looks the kind up here instead of assuming `cog`.
8//!
9//! Registration is explicit: [`KindRegistry::builtin`] builds a registry
10//! holding the in-tree kinds (`cog`), and callers add more with
11//! [`KindRegistry::register`]. There is no global mutable state.
12
13use std::collections::BTreeMap;
14use std::path::Path;
15use std::sync::Arc;
16
17#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
18use clawft_types::placement::engine::WorkloadSpec;
19
20#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
21use crate::workload_ctl::PlaneError;
22#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
23use crate::workload_pkg::TrustAnchors;
24use crate::workload_pkg::{ManifestEnvelope, ManifestError};
25#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
26use crate::workload_runtime::VerifiedWorkload;
27
28mod cog;
29mod health;
30mod project;
31
32#[cfg(test)]
33mod tests;
34#[cfg(all(test, feature = "workload-runtime", feature = "mesh", unix, feature = "native"))]
35mod project_tests;
36
37pub use cog::CogKind;
38pub use health::{Health, HealthSample, HealthSpec, SampleState, judge_process};
39#[cfg(all(feature = "workload-runtime", feature = "mesh", unix, feature = "native"))]
40pub use project::{ProjectFacts, ProjectPrepareError, prepare_project};
41pub use project::{KIND_PROJECT, ProjectKind, project_healthy};
42#[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
43pub use cog::load_cog_shaped;
44
45/// Longest kind id accepted from a manifest (and the most unauthenticated
46/// kind text ever echoed into an error or chain event).
47pub const MAX_KIND_LEN: usize = 32;
48
49/// No kind is registered under this id (the id is truncated to
50/// [`MAX_KIND_LEN`] characters).
51#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
52#[error("unknown workload kind {0:?}")]
53pub struct UnknownKind(pub String);
54
55/// A kind id was registered twice.
56#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
57#[error("workload kind {0:?} is already registered")]
58pub struct DuplicateKind(pub String);
59
60/// What a workload kind contributes to validation and placement.
61pub trait WorkloadKind: Send + Sync {
62    /// The manifest `kind` string this implementation handles.
63    fn id(&self) -> &'static str;
64
65    /// Validate the envelope's kind-specific body (shape and limits; not
66    /// signatures or file contents, which are kind-agnostic).
67    fn validate(&self, envelope: &ManifestEnvelope) -> Result<(), ManifestError>;
68
69    /// How often instances of this kind are polled and how many bad polls
70    /// in a row make one unhealthy (ADR-099 section 7).
71    fn health(&self) -> HealthSpec {
72        HealthSpec::default()
73    }
74
75    /// Judge one poll of an instance of this kind. The default is the
76    /// process-backed judgment ([`judge_process`]).
77    fn judge(&self, sample: &HealthSample) -> Health {
78        judge_process(sample)
79    }
80
81    /// Whether the placer may move an instance of this kind to another
82    /// node when its node is lost. A kind with state that cannot move (warm
83    /// inference caches, hardware-bound work) says `false`: the loss is
84    /// raised as an alert instead of being rescheduled.
85    fn migratable(&self) -> bool {
86        true
87    }
88
89    #[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
90    /// Verify the signed package in `package_dir` against `anchors` (with
91    /// `kinds` as the registry the manifest is checked against) and load it
92    /// as a runnable workload. Must refuse a manifest whose kind is not
93    /// [`id`](Self::id).
94    fn load(
95        &self,
96        package_dir: &Path,
97        anchors: &TrustAnchors,
98        kinds: &KindRegistry,
99    ) -> Result<VerifiedWorkload, PlaneError>;
100
101    #[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
102    /// The placement requirements for a loaded workload of this kind.
103    fn spec(&self, workload: &VerifiedWorkload) -> Result<WorkloadSpec, String>;
104
105    #[cfg(all(feature = "workload-runtime", feature = "mesh", unix))]
106    /// Adapter route names that can run this kind (`native`, `container`,
107    /// `emulated`, `remote.api`, ...).
108    fn adapters(&self) -> &'static [&'static str];
109}
110
111/// Kinds by id.
112#[derive(Clone, Default)]
113pub struct KindRegistry {
114    kinds: BTreeMap<&'static str, Arc<dyn WorkloadKind>>,
115}
116
117impl KindRegistry {
118    /// An empty registry.
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// The in-tree kinds: `cog` and `project`.
124    pub fn builtin() -> Self {
125        let mut r = Self::new();
126        r.register(Arc::new(CogKind))
127            .expect("builtin kind ids are distinct");
128        r.register(Arc::new(ProjectKind))
129            .expect("builtin kind ids are distinct");
130        r
131    }
132
133    /// Add a kind; refuses a second registration of the same id.
134    pub fn register(&mut self, kind: Arc<dyn WorkloadKind>) -> Result<(), DuplicateKind> {
135        let id = kind.id();
136        if self.kinds.contains_key(id) {
137            return Err(DuplicateKind(id.to_string()));
138        }
139        self.kinds.insert(id, kind);
140        Ok(())
141    }
142
143    /// The kind registered under `id`.
144    pub fn get(&self, id: &str) -> Option<&Arc<dyn WorkloadKind>> {
145        self.kinds.get(id)
146    }
147
148    /// Like [`get`](Self::get) with a structured error for an unknown id.
149    pub fn require(&self, id: &str) -> Result<&Arc<dyn WorkloadKind>, UnknownKind> {
150        self.get(id)
151            .ok_or_else(|| UnknownKind(id.chars().take(MAX_KIND_LEN).collect()))
152    }
153
154    /// Registered ids in order.
155    pub fn ids(&self) -> Vec<&'static str> {
156        self.kinds.keys().copied().collect()
157    }
158}
159
160impl std::fmt::Debug for KindRegistry {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        f.debug_set().entries(self.kinds.keys()).finish()
163    }
164}
165
166/// Validate an envelope through the registry: the kind must be
167/// registered and its body must pass that kind's validation.
168pub fn validate_envelope(
169    registry: &KindRegistry,
170    envelope: &ManifestEnvelope,
171) -> Result<(), ManifestError> {
172    registry.require(&envelope.kind)?.validate(envelope)
173}
174
175/// The `kind` a package directory's manifest declares, if the manifest is
176/// a bounded regular file that parses far enough to say and the kind is a
177/// valid token of at most [`MAX_KIND_LEN`] characters. `None` leaves the
178/// failure to package verification, which reports it precisely. The result
179/// is unauthenticated: it only selects which registered kind verifies the
180/// package, and that kind re-checks the verified envelope.
181pub fn peek_manifest_kind(package_dir: &Path) -> Option<String> {
182    use crate::workload_pkg::verify::read_bounded;
183    use crate::workload_pkg::{MANIFEST_FILE, MAX_MANIFEST_BYTES};
184    let bytes = read_bounded(
185        &package_dir.join(MANIFEST_FILE),
186        MANIFEST_FILE,
187        MAX_MANIFEST_BYTES as u64,
188    )
189    .ok()?;
190    peek_kind_in(&bytes)
191}
192
193/// [`peek_manifest_kind`] over manifest bytes the caller already read, so
194/// one read serves the peek and the verification that follows.
195pub fn peek_kind_in(manifest: &[u8]) -> Option<String> {
196    use crate::workload_pkg::manifest::valid_token;
197    let doc: serde_json::Value = serde_json::from_slice(manifest).ok()?;
198    let kind = doc.get("kind")?.as_str()?;
199    valid_token(kind, MAX_KIND_LEN).then(|| kind.to_string())
200}