Skip to main content

nxd_core/application/
contracts.rs

1use crate::contract::API_VERSION;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::path::PathBuf;
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum ConfigSource {
8	NixInstallable(String),
9	CanonicalJson(PathBuf),
10}
11
12#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
13#[serde(rename_all = "camelCase", deny_unknown_fields)]
14pub struct ValidationReport {
15	pub api_version: String,
16	pub kind: ValidationReportKind,
17	pub metadata: ValidationReportMetadata,
18	pub spec: ValidationReportSpec,
19}
20
21#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
22pub enum ValidationReportKind {
23	ValidationReport,
24}
25
26#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
27#[serde(deny_unknown_fields)]
28pub struct ValidationReportMetadata {}
29
30#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
31#[serde(rename_all = "camelCase", deny_unknown_fields)]
32pub struct ValidationReportSpec {
33	pub resource_count: usize,
34}
35
36impl ValidationReport {
37	pub(super) fn new(resource_count: usize) -> Self {
38		Self {
39			api_version: API_VERSION.to_string(),
40			kind: ValidationReportKind::ValidationReport,
41			metadata: ValidationReportMetadata {},
42			spec: ValidationReportSpec { resource_count },
43		}
44	}
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct ValidateRequest {
49	pub source: ConfigSource,
50	pub working_root: PathBuf,
51	pub selectors: Vec<String>,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct PlanRequest {
56	pub source: ConfigSource,
57	/// Explicit root for operational inputs referenced by the request.
58	pub working_root: PathBuf,
59	pub selectors: Vec<String>,
60	pub lifecycle_intent: Option<crate::host_lifecycle::LifecycleIntent>,
61	/// Existing-guest policy for install plans. Invalid for other lifecycles.
62	pub install_mode: Option<crate::host_lifecycle::InstallMode>,
63	/// Confidential enrollment delivery for install/convert. Invalid for every
64	/// other lifecycle and persisted in the resulting reviewed plan.
65	pub enrollment_strategy: Option<crate::host_lifecycle::EnrollmentStrategy>,
66	/// Request activation of the exact reviewed system even when it is already
67	/// active. Valid only for the switch lifecycle; never bypasses policy.
68	pub reactivate: bool,
69	/// Request Replace host plane enrollment: remint, sink overwrite,
70	/// exact node cleanup, and Nix re-login. Valid for switch; identity-critical.
71	pub reenroll: bool,
72	/// Explicit source identity for convert plans (for example `[email protected]`).
73	pub convert_from: Option<String>,
74	/// Explicit metadata-only activation plan mode; never inferred from environment.
75	pub offline: bool,
76	/// Explicit stable SSH host-identity maintenance. Valid only without a
77	/// lifecycle intent and never inferred from observed drift.
78	pub host_identity_action: Option<crate::host_lifecycle::HostIdentityAction>,
79	/// Explicit operation-scoped hostname -> IP overrides. Values are folded
80	/// into the effective canonical config and therefore into the plan digest.
81	pub endpoint_overrides: BTreeMap<String, String>,
82}
83
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct InfoRequest {
86	pub source: ConfigSource,
87	pub target: String,
88	/// Explicitly request a live endpoint observation from the owning provider.
89	/// Plain info remains a canonical inventory query.
90	pub observe_ip: bool,
91	/// Poll retryable readiness failures within the bounded observation window.
92	pub wait: bool,
93}
94
95#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
96#[serde(rename_all = "camelCase", deny_unknown_fields)]
97pub struct InfoReport {
98	pub hostname: String,
99	#[serde(skip_serializing_if = "Option::is_none")]
100	pub ip: Option<String>,
101	pub user: String,
102	pub class: String,
103	pub system: String,
104	pub vmid: String,
105	pub provider: String,
106}
107
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub struct ApplyRequest {
110	pub plan: PathBuf,
111	pub run_id: Option<String>,
112	pub approval_evidence: Option<PathBuf>,
113	pub parallel: usize,
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
117pub struct ArtifactBuildRequest {
118	pub selector: String,
119	pub source: ConfigSource,
120	pub working_root: PathBuf,
121	pub output_root: PathBuf,
122	pub parallel: usize,
123}
124
125#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
126#[serde(rename_all = "camelCase", deny_unknown_fields)]
127pub struct ArtifactBuildOutput {
128	pub role: String,
129	pub path: String,
130}
131
132#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
133#[serde(rename_all = "camelCase", deny_unknown_fields)]
134pub struct ArtifactBuildReport {
135	pub plan_path: String,
136	pub manifest_path: String,
137	pub run_id: String,
138	pub applied_action_count: usize,
139	pub outputs: Vec<ArtifactBuildOutput>,
140}
141
142#[derive(Clone, Debug, PartialEq, Eq)]
143pub struct CreateApprovalRequest {
144	pub plan: PathBuf,
145	pub principal: String,
146	/// Optional absolute expiry; defaults to min(now+1h, plan.expires_at_unix).
147	pub expires_at_unix: Option<u64>,
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
151pub struct ValidateApprovalRequest {
152	pub plan: PathBuf,
153	pub approval_evidence: PathBuf,
154}
155
156#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
157#[serde(rename_all = "camelCase", deny_unknown_fields)]
158pub struct ApplyReport {
159	pub run_id: String,
160	pub applied_action_count: usize,
161	pub status: ApplyStatus,
162}
163
164/// Plan document annotated with the apply-bound digest for operator approval.
165#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
166#[serde(rename_all = "camelCase")]
167pub struct PlanDisplay {
168	#[serde(flatten)]
169	pub plan: crate::plan::PlanEnvelope,
170	pub plan_digest: String,
171}
172
173#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
174#[serde(rename_all = "camelCase", deny_unknown_fields)]
175pub struct CancelRunReport {
176	pub run_id: String,
177	pub status: String,
178}
179
180impl CancelRunReport {
181	pub fn requested(run_id: impl Into<String>) -> Self {
182		Self { run_id: run_id.into(), status: "cancellation-requested".to_string() }
183	}
184}
185
186#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
187#[serde(rename_all = "camelCase", deny_unknown_fields)]
188pub struct MonitorRunReport {
189	pub run_id: String,
190	pub final_status: String,
191	pub action_status: BTreeMap<String, String>,
192	pub errors: Vec<String>,
193}
194
195#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
196#[serde(rename_all = "kebab-case")]
197pub enum ApplyStatus {
198	Succeeded,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq)]
202pub struct VerifyRequest {
203	pub source: ConfigSource,
204	/// Explicit root for operational inputs referenced by verification.
205	pub working_root: PathBuf,
206	pub selectors: Vec<String>,
207	pub verbose: bool,
208}
209
210#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
211#[serde(rename_all = "camelCase", deny_unknown_fields)]
212pub struct VerificationReport {
213	pub api_version: String,
214	pub kind: VerificationReportKind,
215	pub metadata: VerificationReportMetadata,
216	pub spec: VerificationReportSpec,
217}
218
219#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
220pub enum VerificationReportKind {
221	VerificationReport,
222}
223
224#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
225#[serde(deny_unknown_fields)]
226pub struct VerificationReportMetadata {}
227
228#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
229#[serde(rename_all = "camelCase", deny_unknown_fields)]
230pub struct VerificationReportSpec {
231	pub resource_count: usize,
232	pub status: VerificationStatus,
233}
234
235#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
236#[serde(rename_all = "kebab-case")]
237pub enum VerificationStatus {
238	Passed,
239}
240
241impl VerificationReport {
242	pub(super) fn passed(resource_count: usize) -> Self {
243		Self {
244			api_version: API_VERSION.to_string(),
245			kind: VerificationReportKind::VerificationReport,
246			metadata: VerificationReportMetadata {},
247			spec: VerificationReportSpec { resource_count, status: VerificationStatus::Passed },
248		}
249	}
250}
251
252#[derive(Clone, Debug, PartialEq, Eq)]
253pub struct CaptureRequest {
254	pub source: ConfigSource,
255	pub selectors: Vec<String>,
256	pub output_root: Option<PathBuf>,
257}
258
259#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
260#[serde(rename_all = "camelCase", deny_unknown_fields)]
261pub struct CaptureReport {
262	pub status: CaptureStatus,
263	pub outcomes: Vec<CaptureOutcome>,
264}
265
266#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
267#[serde(rename_all = "kebab-case")]
268pub enum CaptureStatus {
269	Complete,
270	Incomplete,
271}
272
273#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
274#[serde(rename_all = "camelCase", deny_unknown_fields)]
275pub struct CaptureOutcome {
276	pub node: String,
277	pub artifact: String,
278	pub status: CaptureOutcomeStatus,
279}
280
281#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
282#[serde(rename_all = "camelCase")]
283pub enum CaptureOutcomeStatus {
284	Captured,
285	Skipped { reason: String },
286	Failed { error: String },
287}
288
289impl CaptureReport {
290	pub fn failed_count(&self) -> usize {
291		self
292			.outcomes
293			.iter()
294			.filter(|outcome| matches!(outcome.status, CaptureOutcomeStatus::Failed { .. }))
295			.count()
296	}
297}
298
299#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
300#[serde(rename_all = "camelCase", deny_unknown_fields)]
301pub struct OperationReport {
302	pub api_version: String,
303	pub kind: OperationReportKind,
304	pub metadata: OperationReportMetadata,
305	pub spec: OperationReportSpec,
306}
307
308#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
309pub enum OperationReportKind {
310	OperationReport,
311}
312
313#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
314#[serde(deny_unknown_fields)]
315pub struct OperationReportMetadata {}
316
317#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
318#[serde(rename_all = "camelCase", deny_unknown_fields)]
319pub struct OperationReportSpec {
320	pub operation: String,
321	pub resource_count: usize,
322	pub action_count: usize,
323	pub highest_risk: Option<String>,
324	pub status: String,
325}
326
327impl OperationReport {
328	pub fn new(
329		operation: impl Into<String>,
330		resource_count: usize,
331		action_count: usize,
332		highest_risk: Option<String>,
333		status: impl Into<String>,
334	) -> Self {
335		Self {
336			api_version: API_VERSION.to_string(),
337			kind: OperationReportKind::OperationReport,
338			metadata: OperationReportMetadata {},
339			spec: OperationReportSpec {
340				operation: operation.into(),
341				resource_count,
342				action_count,
343				highest_risk,
344				status: status.into(),
345			},
346		}
347	}
348}