Skip to main content

harn_hostlib/host_lease/
execution.rs

1//! Typed, redacted workload identity for machine-resource lease receipts.
2
3use std::path::Path;
4
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7
8use super::{HostLeasePriorityClass, HostLeaseResourceKey};
9
10const EXECUTION_CONTEXT_SCHEMA_VERSION: u32 = 1;
11
12/// One-way, machine-local identity for a filesystem path.
13///
14/// Receipts can correlate repeated work without publishing a developer's
15/// workspace layout. Callers must canonicalize the path before constructing
16/// the identity when spelling-independent correlation is required.
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct HostLeasePathIdentity {
19    /// SHA-256 digest of the platform path encoding.
20    pub sha256: String,
21}
22
23impl HostLeasePathIdentity {
24    /// Construct a redacted identity without persisting the source path.
25    pub fn from_path(path: &Path) -> Self {
26        let digest = Sha256::digest(path.as_os_str().as_encoded_bytes());
27        Self {
28            sha256: hex::encode(digest),
29        }
30    }
31}
32
33/// Operation kind represented by a supervised lease receipt.
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "kebab-case")]
36pub enum HostLeaseOperationKind {
37    /// A Cargo build, test, check, or code-generation workload.
38    Cargo,
39}
40
41/// Cargo-specific context nested beneath a generic execution receipt.
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
43pub struct HostLeaseCargoExecutionContext {
44    /// Canonical workspace identity supplied to Cargo.
45    pub workspace: HostLeasePathIdentity,
46    /// Isolated final-artifact directory identity.
47    pub target_dir: HostLeasePathIdentity,
48    /// Intermediate-artifact directory identity when explicitly selected.
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub build_dir: Option<HostLeasePathIdentity>,
51}
52
53/// Versioned, redacted execution context stored with a supervised lease.
54///
55/// The tagged enum makes operation-specific fields structurally mandatory;
56/// callers cannot claim a Cargo operation while omitting its Cargo context.
57#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(tag = "operation_kind", rename_all = "kebab-case")]
59pub enum HostLeaseExecutionContext {
60    /// Cargo execution context.
61    Cargo {
62        /// Contract version for this nested receipt.
63        schema_version: u32,
64        /// Redacted Cargo path identities.
65        context: HostLeaseCargoExecutionContext,
66    },
67}
68
69impl HostLeaseExecutionContext {
70    /// Build a Cargo context while keeping raw local paths out of receipts.
71    pub fn cargo(workspace: &Path, target_dir: &Path, build_dir: Option<&Path>) -> Self {
72        Self::Cargo {
73            schema_version: EXECUTION_CONTEXT_SCHEMA_VERSION,
74            context: HostLeaseCargoExecutionContext {
75                workspace: HostLeasePathIdentity::from_path(workspace),
76                target_dir: HostLeasePathIdentity::from_path(target_dir),
77                build_dir: build_dir.map(HostLeasePathIdentity::from_path),
78            },
79        }
80    }
81
82    /// Stable operation kind for scheduling and receipt projections.
83    pub const fn operation_kind(&self) -> HostLeaseOperationKind {
84        match self {
85            Self::Cargo { .. } => HostLeaseOperationKind::Cargo,
86        }
87    }
88
89    /// Cargo context when this receipt represents Cargo work.
90    pub const fn cargo_context(&self) -> Option<&HostLeaseCargoExecutionContext> {
91        match self {
92            Self::Cargo { context, .. } => Some(context),
93        }
94    }
95}
96
97/// Terminal child status recorded by a supervised lease run.
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99pub struct HostLeaseProcessExit {
100    /// Process exit code when the child exited normally.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub code: Option<i32>,
103    /// Terminating Unix signal, absent on normal and non-Unix exits.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub signal: Option<i32>,
106}
107
108/// Outcome of releasing a workload lease after its process exits.
109#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
110#[serde(rename_all = "kebab-case")]
111pub enum HostLeaseRunReleaseOutcome {
112    /// The supervisor deleted the matching active lease.
113    Released,
114    /// Another transaction had already recovered the now-dead owner.
115    AlreadyRecovered,
116}
117
118/// Stable category for a failure before workload execution owns a lease.
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "kebab-case")]
121pub enum HostLeaseRunStartFailure {
122    /// The supervisor could not encode the worker invocation.
123    WorkerArguments,
124    /// The supervisor could not create the worker process.
125    WorkerSpawn,
126    /// The worker exited before recording lease acquisition or deferral.
127    WorkerExitedBeforeAcquire,
128    /// The worker invocation disagreed with its durable receipt.
129    WorkerContextMismatch,
130    /// The resource store could not complete acquisition.
131    ResourceAcquire,
132    /// An acquired receipt omitted required ownership evidence.
133    WorkerContract,
134    /// The acquired receipt could not advance to running.
135    ReceiptTransition,
136}
137
138/// Stable category for a failure after acquisition but before execution.
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "kebab-case")]
141pub enum HostLeaseRunLaunchFailure {
142    /// A process argument could not be represented by the host process API.
143    ArgumentEncoding,
144    /// Replacing the worker with the workload failed.
145    ProcessReplace,
146    /// Creating the workload process failed.
147    ProcessSpawn,
148    /// The platform process-tree guard could not be established.
149    ProcessSupervision,
150    /// The platform has no proven supervised-workload implementation.
151    UnsupportedPlatform,
152}
153
154/// Lifecycle state for one supervised workload.
155#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(tag = "state", rename_all = "kebab-case")]
157pub enum HostLeaseRunState {
158    /// The supervisor persisted intent before creating the worker.
159    Pending {
160        /// Receipt creation time in Unix milliseconds.
161        requested_at_ms: i64,
162    },
163    /// The worker owns the lease and is about to advance the workload.
164    Running {
165        /// Opaque lease token.
166        lease_id: String,
167        /// Lease acquisition time in Unix milliseconds.
168        acquired_at_ms: i64,
169        /// Time spent waiting for the resource.
170        acquire_wait_ms: u64,
171        /// PID whose liveness owns the lease.
172        worker_pid: u32,
173    },
174    /// The resource remained held through the requested wait deadline.
175    Deferred {
176        /// Observation time in Unix milliseconds.
177        observed_at_ms: i64,
178        /// Time spent waiting before deferral.
179        waited_ms: u64,
180    },
181    /// The worker could not reach lease ownership.
182    StartFailed {
183        /// Observation time in Unix milliseconds.
184        observed_at_ms: i64,
185        /// Stable failure category without raw command or path text.
186        error: HostLeaseRunStartFailure,
187    },
188    /// The supervisor cancelled the worker before it acquired the resource.
189    CancelledBeforeStart {
190        /// Terminal observation time in Unix milliseconds.
191        finished_at_ms: i64,
192    },
193    /// Command preparation failed after acquisition but before execution.
194    LaunchFailed {
195        /// Opaque lease token acquired for the failed launch.
196        lease_id: String,
197        /// Lease cleanup result after the launch was abandoned.
198        release: HostLeaseRunReleaseOutcome,
199        /// Observation time in Unix milliseconds.
200        observed_at_ms: i64,
201        /// Stable failure category without raw command or path text.
202        error: HostLeaseRunLaunchFailure,
203    },
204    /// The workload process reached a terminal status.
205    Completed {
206        /// Opaque lease token retained for correlation.
207        lease_id: String,
208        /// Time spent waiting for the resource.
209        acquire_wait_ms: u64,
210        /// Time from acquisition until workload exit.
211        hold_ms: u64,
212        /// PID whose liveness owned the lease.
213        worker_pid: u32,
214        /// Workload exit status.
215        exit: HostLeaseProcessExit,
216        /// Lease cleanup result after the process was confirmed dead.
217        release: HostLeaseRunReleaseOutcome,
218        /// Terminal observation time in Unix milliseconds.
219        finished_at_ms: i64,
220    },
221    /// The supervisor interrupted and reaped the workload process group.
222    Cancelled {
223        /// Opaque lease token retained for correlation.
224        lease_id: String,
225        /// Time spent waiting for the resource.
226        acquire_wait_ms: u64,
227        /// Time from acquisition until cancellation completed.
228        hold_ms: u64,
229        /// PID whose liveness owned the lease.
230        worker_pid: u32,
231        /// Lease cleanup result after process-tree cleanup.
232        release: HostLeaseRunReleaseOutcome,
233        /// Terminal observation time in Unix milliseconds.
234        finished_at_ms: i64,
235    },
236}
237
238impl HostLeaseRunState {
239    pub(super) fn may_transition_to(&self, next: &Self) -> bool {
240        match (self, next) {
241            (
242                Self::Pending { .. },
243                Self::Running { .. }
244                | Self::Deferred { .. }
245                | Self::StartFailed { .. }
246                | Self::CancelledBeforeStart { .. },
247            ) => true,
248            (
249                Self::Running {
250                    lease_id,
251                    acquire_wait_ms,
252                    worker_pid,
253                    ..
254                },
255                Self::Completed {
256                    lease_id: next_lease_id,
257                    acquire_wait_ms: next_wait_ms,
258                    worker_pid: next_worker_pid,
259                    ..
260                }
261                | Self::Cancelled {
262                    lease_id: next_lease_id,
263                    acquire_wait_ms: next_wait_ms,
264                    worker_pid: next_worker_pid,
265                    ..
266                },
267            ) => {
268                lease_id == next_lease_id
269                    && acquire_wait_ms == next_wait_ms
270                    && worker_pid == next_worker_pid
271            }
272            (
273                Self::Running { lease_id, .. },
274                Self::LaunchFailed {
275                    lease_id: next_lease_id,
276                    ..
277                },
278            ) => lease_id == next_lease_id,
279            _ => false,
280        }
281    }
282}
283
284/// Durable lifecycle evidence for one supervised workload.
285#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
286pub struct HostLeaseRunReceipt {
287    /// Contract version for this receipt.
288    pub schema_version: u32,
289    /// Stable run identifier and receipt filename stem.
290    pub run_id: String,
291    /// Validated operator or automation identity that requested the run.
292    pub owner: String,
293    /// Scheduling class requested for this run and used by its worker.
294    pub priority_class: HostLeasePriorityClass,
295    /// Maximum resource wait requested by the public supervisor.
296    pub wait_limit_ms: u64,
297    /// Capacity-one machine resource used by the workload.
298    pub resource: HostLeaseResourceKey,
299    /// Typed, redacted workload identity.
300    pub execution_context: HostLeaseExecutionContext,
301    /// Current durable lifecycle state.
302    pub status: HostLeaseRunState,
303}
304
305#[cfg(test)]
306mod tests {
307    use std::path::Path;
308
309    use super::*;
310
311    #[test]
312    fn cargo_context_keeps_only_stable_path_identities() {
313        let context = HostLeaseExecutionContext::cargo(
314            Path::new("/workspace/project"),
315            Path::new("/tmp/target"),
316            Some(Path::new("/tmp/build")),
317        );
318
319        assert_eq!(context.operation_kind(), HostLeaseOperationKind::Cargo);
320        let cargo = context.cargo_context().expect("Cargo context is present");
321        assert_ne!(cargo.target_dir, cargo.build_dir.clone().unwrap());
322        assert_eq!(cargo.workspace.sha256.len(), 64);
323    }
324
325    #[test]
326    fn omitted_build_directory_is_not_invented() {
327        let context = HostLeaseExecutionContext::cargo(
328            Path::new("/workspace/project"),
329            Path::new("/tmp/target"),
330            None,
331        );
332
333        assert!(context
334            .cargo_context()
335            .expect("Cargo context is present")
336            .build_dir
337            .is_none());
338    }
339}