1use crate::limits::*;
7use crate::*;
8
9fn validate_report_page_limit(
10 value: u32,
11 max: u32,
12 field: &'static str,
13) -> Result<(), WireValidationError> {
14 if value == 0 || value > max {
15 return Err(WireValidationError {
16 field,
17 message: format!("value is {value}, must be 1..={max}"),
18 });
19 }
20 Ok(())
21}
22
23impl WireValidate for RegisterPayload {
26 fn wire_validate(&self) -> Result<(), WireValidationError> {
27 Ok(())
28 }
29}
30
31impl WireValidate for ServiceMessage {
32 fn wire_validate(&self) -> Result<(), WireValidationError> {
33 match self {
34 ServiceMessage::Ping(_) => Ok(()),
35 ServiceMessage::Register(_) => Ok(()),
36 ServiceMessage::Enroll(p) => p.wire_validate(),
37 ServiceMessage::RequestCertificate(p) => p.wire_validate(),
38 ServiceMessage::RenewCertificate(p) => p.wire_validate(),
39 ServiceMessage::ReportHosts(p) => p.wire_validate(),
40 ServiceMessage::VersionCheckResults(p) => p.wire_validate(),
41 ServiceMessage::UpdateStarted(p) => p.wire_validate(),
42 ServiceMessage::UpdateOutput(p) => p.wire_validate(),
43 ServiceMessage::UpdateResult(p) => p.wire_validate(),
44 ServiceMessage::BatchUpdateResult(p) => p.wire_validate(),
45 ServiceMessage::DiscoveryResults(p) => p.wire_validate(),
46 ServiceMessage::StdinAttention(p) => p.wire_validate(),
47 ServiceMessage::ServiceTriggerUpdate(p) => p.wire_validate(),
48 ServiceMessage::ServiceTriggerHostBatchUpdate(_) => Ok(()),
49 ServiceMessage::Disconnecting(p) => p.wire_validate(),
50 ServiceMessage::ReportPluginConfig(p) => p.wire_validate(),
51 ServiceMessage::SurfaceRegistration(p) => p.wire_validate(),
52 ServiceMessage::SurfaceActionResponse(p) => p.wire_validate(),
53 ServiceMessage::SurfaceActionRequest(p) => p.wire_validate(),
54 ServiceMessage::StoreServiceConfig(p) => p.wire_validate(),
55 ServiceMessage::DeleteServiceConfig(p) => p.wire_validate(),
56 ServiceMessage::WorkloadClaim(p) => p.wire_validate(),
57 ServiceMessage::WorkloadRelease(p) => p.wire_validate(),
58 ServiceMessage::TestPluginConfigResult(p) => p.wire_validate(),
59 ServiceMessage::AuditEvent(_) => Ok(()),
60 _ => {
62 tracing::debug!(
63 "received unknown ServiceMessage variant from peer; skipping validation"
64 );
65 Ok(())
66 }
67 }
68 }
69}
70
71impl WireValidate for ControllerMessage {
74 fn wire_validate(&self) -> Result<(), WireValidationError> {
75 match self {
76 ControllerMessage::Pong(_) => Ok(()),
77 ControllerMessage::Enrolled(_) => Ok(()),
78 ControllerMessage::Approved(_) => Ok(()),
79 ControllerMessage::Rejected(_) => Ok(()),
80 ControllerMessage::Certificate(p) => p.wire_validate(),
81 ControllerMessage::Error(p) => p.wire_validate(),
82 ControllerMessage::ServiceSettings(p) => p.wire_validate(),
83 ControllerMessage::CaBundleUpdated(p) => p.wire_validate(),
84 ControllerMessage::RequestCertRenewal(p) => p.wire_validate(),
85 ControllerMessage::ServerRestarting(p) => p.wire_validate(),
86 ControllerMessage::CheckVersions(p) => p.wire_validate(),
87 ControllerMessage::ExecuteUpdate(p) => p.wire_validate(),
88 ControllerMessage::ExecuteBatchUpdate(p) => p.wire_validate(),
89 ControllerMessage::DiscoverSoftware(p) => p.wire_validate(),
90 ControllerMessage::SetUpdateFreeze(p) => p.wire_validate(),
91 ControllerMessage::UpdateStdinData(p) => p.wire_validate(),
92 ControllerMessage::SoftwareStates(p) => p.wire_validate(),
93 ControllerMessage::HostConnectivityUpdated(p) => p.wire_validate(),
94 ControllerMessage::ReportPluginConfigResponse(p) => p.wire_validate(),
95 ControllerMessage::SurfaceActionRequest(p) => p.wire_validate(),
96 ControllerMessage::SurfaceActionCancel(p) => p.wire_validate(),
97 ControllerMessage::SurfaceActionResponse(p) => p.wire_validate(),
98 ControllerMessage::ServiceCredentials(_) => Ok(()),
99 ControllerMessage::ServiceConfigDelivery(p) => p.wire_validate(),
100 ControllerMessage::ServiceConfigAck(p) => p.wire_validate(),
101 ControllerMessage::ServiceConfigUpdated(p) => p.wire_validate(),
102 ControllerMessage::RequestCaRotation(p) => p.wire_validate(),
103 ControllerMessage::RequestCrlRenewal(_) => Ok(()),
104 ControllerMessage::TokenRevoked(_) => Ok(()),
105 ControllerMessage::WorkloadClaimResult(p) => p.wire_validate(),
106 ControllerMessage::WorkloadClaimAnnouncement(p) => p.wire_validate(),
107 ControllerMessage::WorkloadClaimSyncRequest(_) => Ok(()),
108 ControllerMessage::WorkloadClaimSyncResponse(p) => p.wire_validate(),
109 ControllerMessage::TestPluginConfig(p) => p.wire_validate(),
110 _ => {
112 tracing::debug!(
113 "received unknown ControllerMessage variant from peer; skipping validation"
114 );
115 Ok(())
116 }
117 }
118 }
119}
120
121impl WireValidate for crate::envelope::ReportPagination {
124 fn wire_validate(&self) -> Result<(), WireValidationError> {
125 if self.total_pages == 0 || self.total_pages > MAX_REPORT_PAGES {
126 return Err(WireValidationError {
127 field: "pagination.total_pages",
128 message: format!(
129 "total_pages is {}, must be 1..={MAX_REPORT_PAGES}",
130 self.total_pages
131 ),
132 });
133 }
134 if self.page == 0 || self.page > self.total_pages {
135 return Err(WireValidationError {
136 field: "pagination.page",
137 message: format!("page is {}, must be 1..={}", self.page, self.total_pages),
138 });
139 }
140 Ok(())
141 }
142}
143
144impl WireValidate for EnrollPayload {
147 fn wire_validate(&self) -> Result<(), WireValidationError> {
148 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
149 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
150 check_string_len(
151 &self.service_app_name,
152 MAX_SHORT_STRING_LEN,
153 "service_app_name",
154 )?;
155 Ok(())
156 }
157}
158
159impl WireValidate for RequestCertificatePayload {
160 fn wire_validate(&self) -> Result<(), WireValidationError> {
161 check_string_len(&self.csr_pem, MAX_LONG_STRING_LEN, "csr_pem")?;
162 Ok(())
163 }
164}
165
166impl WireValidate for RenewCertificatePayload {
167 fn wire_validate(&self) -> Result<(), WireValidationError> {
168 check_string_len(&self.csr_pem, MAX_LONG_STRING_LEN, "csr_pem")?;
169 Ok(())
170 }
171}
172
173impl WireValidate for ReportHostsPayload {
174 fn wire_validate(&self) -> Result<(), WireValidationError> {
175 check_vec_len(&self.hosts, MAX_REPORT_HOSTS, "hosts")?;
176 check_string_len(&self.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
177 for host in &self.hosts {
178 host.wire_validate()?;
179 }
180 Ok(())
181 }
182}
183
184impl WireValidate for HostInfo {
185 fn wire_validate(&self) -> Result<(), WireValidationError> {
186 check_string_len(&self.machine_id, MAX_SHORT_STRING_LEN, "machine_id")?;
187 check_opt_string_len(&self.os_type, MAX_SHORT_STRING_LEN, "os_type")?;
188 check_opt_string_len(&self.os_version, MAX_SHORT_STRING_LEN, "os_version")?;
189 check_opt_string_len(&self.architecture, MAX_SHORT_STRING_LEN, "architecture")?;
190 check_opt_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
191 check_opt_string_len(&self.ip_address, MAX_SHORT_STRING_LEN, "ip_address")?;
192 Ok(())
193 }
194}
195
196impl WireValidate for VersionCheckResultsPayload {
197 fn wire_validate(&self) -> Result<(), WireValidationError> {
198 check_vec_len(&self.results, MAX_VERSION_CHECK_RESULTS, "results")?;
199 for result in &self.results {
200 result.wire_validate()?;
201 }
202 Ok(())
203 }
204}
205
206impl WireValidate for VersionCheckResult {
207 fn wire_validate(&self) -> Result<(), WireValidationError> {
208 check_opt_string_len(
209 &self.installed_version,
210 MAX_SHORT_STRING_LEN,
211 "installed_version",
212 )?;
213 check_opt_string_len(&self.latest_version, MAX_SHORT_STRING_LEN, "latest_version")?;
214 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
215 Ok(())
216 }
217}
218
219impl WireValidate for UpdateStartedPayload {
220 fn wire_validate(&self) -> Result<(), WireValidationError> {
221 check_opt_string_len(&self.from_version, MAX_SHORT_STRING_LEN, "from_version")?;
222 Ok(())
223 }
224}
225
226impl WireValidate for UpdateOutputPayload {
227 fn wire_validate(&self) -> Result<(), WireValidationError> {
228 check_string_len(&self.output, MAX_OUTPUT_STRING_LEN, "output")?;
229 Ok(())
230 }
231}
232
233impl WireValidate for UpdateResultPayload {
234 fn wire_validate(&self) -> Result<(), WireValidationError> {
235 check_string_len(&self.output, MAX_OUTPUT_STRING_LEN, "output")?;
236 check_opt_string_len(&self.from_version, MAX_SHORT_STRING_LEN, "from_version")?;
237 check_opt_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
238 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
239 Ok(())
240 }
241}
242
243impl WireValidate for BatchUpdateResultPayload {
244 fn wire_validate(&self) -> Result<(), WireValidationError> {
245 check_vec_len(&self.results, MAX_BATCH_UPDATE_RESULTS, "results")?;
246 for result in &self.results {
247 result.wire_validate()?;
248 }
249 Ok(())
250 }
251}
252
253impl WireValidate for BatchUpdateItemResult {
254 fn wire_validate(&self) -> Result<(), WireValidationError> {
255 check_string_len(&self.output, MAX_OUTPUT_STRING_LEN, "output")?;
256 check_opt_string_len(
257 &self.installed_version,
258 MAX_SHORT_STRING_LEN,
259 "installed_version",
260 )?;
261 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
262 Ok(())
263 }
264}
265
266impl WireValidate for DiscoveryResultsPayload {
267 fn wire_validate(&self) -> Result<(), WireValidationError> {
268 check_string_len(
269 &self.host_machine_id,
270 MAX_SHORT_STRING_LEN,
271 "host_machine_id",
272 )?;
273 check_vec_len(&self.results, MAX_DISCOVERY_PLUGIN_RESULTS, "results")?;
274 for result in &self.results {
275 result.wire_validate()?;
276 }
277 Ok(())
278 }
279}
280
281impl WireValidate for DiscoveryPluginResult {
282 fn wire_validate(&self) -> Result<(), WireValidationError> {
283 check_vec_len(&self.discoveries, MAX_DISCOVERIES_PER_PLUGIN, "discoveries")?;
284 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
285 for discovery in &self.discoveries {
286 discovery.wire_validate()?;
287 }
288 Ok(())
289 }
290}
291
292impl WireValidate for uptrakit_shared_types::DiscoveredSoftware {
293 fn wire_validate(&self) -> Result<(), WireValidationError> {
294 check_string_len(
295 &self.package_identifier,
296 MAX_SHORT_STRING_LEN,
297 "package_identifier",
298 )?;
299 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
300 check_string_len(
301 &self.installed_version,
302 MAX_SHORT_STRING_LEN,
303 "installed_version",
304 )?;
305 check_opt_string_len(&self.qualifier, MAX_DISCOVERED_QUALIFIER_LEN, "qualifier")?;
306 check_opt_string_len(
307 &self.plugin_package_identifier,
308 MAX_SHORT_STRING_LEN,
309 "plugin_package_identifier",
310 )?;
311 Ok(())
312 }
313}
314
315impl WireValidate for ServiceUpdateTriggerPayload {
316 fn wire_validate(&self) -> Result<(), WireValidationError> {
317 check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
318 Ok(())
319 }
320}
321
322impl WireValidate for DisconnectingPayload {
323 fn wire_validate(&self) -> Result<(), WireValidationError> {
324 Ok(())
325 }
326}
327
328fn validate_surface_json_bounds(
329 value: &serde_json::Value,
330 field: &'static str,
331) -> Result<(), WireValidationError> {
332 let mut node_count = 0usize;
333 fn walk(
334 value: &serde_json::Value,
335 depth: usize,
336 node_count: &mut usize,
337 field: &'static str,
338 ) -> Result<(), WireValidationError> {
339 if depth > MAX_SURFACE_JSON_DEPTH {
340 return Err(WireValidationError {
341 field,
342 message: format!(
343 "JSON depth exceeds max {MAX_SURFACE_JSON_DEPTH} (observed depth {depth})"
344 ),
345 });
346 }
347
348 *node_count += 1;
349 if *node_count > MAX_SURFACE_JSON_NODES {
350 return Err(WireValidationError {
351 field,
352 message: format!("JSON node count exceeds max {MAX_SURFACE_JSON_NODES}"),
353 });
354 }
355
356 match value {
357 serde_json::Value::Array(items) => {
358 for item in items {
359 walk(item, depth + 1, node_count, field)?;
360 }
361 }
362 serde_json::Value::Object(map) => {
363 for item in map.values() {
364 walk(item, depth + 1, node_count, field)?;
365 }
366 }
367 _ => {}
368 }
369
370 Ok(())
371 }
372
373 walk(value, 1, &mut node_count, field)
374}
375
376fn validate_surface_node(
377 node: &surfaces::SurfaceNode,
378 depth: usize,
379) -> Result<(), WireValidationError> {
380 if depth > MAX_SURFACE_JSON_DEPTH {
381 return Err(WireValidationError {
382 field: "surfaces[].descriptor.root_node",
383 message: format!(
384 "root node depth exceeds max {MAX_SURFACE_JSON_DEPTH} (observed depth {depth})"
385 ),
386 });
387 }
388
389 match node {
390 surfaces::SurfaceNode::Section {
391 title,
392 children,
393 header_action_ids,
394 } => {
395 check_opt_string_len(
396 title,
397 MAX_SHORT_STRING_LEN,
398 "surfaces[].descriptor.root_node.title",
399 )?;
400 if header_action_ids.len() > 3 {
401 return Err(WireValidationError {
402 field: "surfaces[].descriptor.root_node.header_action_ids",
403 message: format!(
404 "section header_action_ids has {} entries, max 3",
405 header_action_ids.len()
406 ),
407 });
408 }
409 check_vec_len(
410 children,
411 MAX_SURFACE_FIELDS,
412 "surfaces[].descriptor.root_node.children",
413 )?;
414 for child in children {
415 validate_surface_node(child, depth + 1)?;
416 }
417 }
418 surfaces::SurfaceNode::TextBlock { text } => {
419 check_string_len(
420 text,
421 MAX_MEDIUM_STRING_LEN,
422 "surfaces[].descriptor.root_node.text",
423 )?;
424 }
425 surfaces::SurfaceNode::KeyValue { .. } | surfaces::SurfaceNode::Table { .. } => {}
426 surfaces::SurfaceNode::Form { .. } => {}
427 surfaces::SurfaceNode::ActionBar { action_ids } => {
428 check_vec_len(
429 action_ids,
430 MAX_SURFACE_ACTION_REFS,
431 "surfaces[].descriptor.root_node.action_ids",
432 )?;
433 }
434 surfaces::SurfaceNode::Tabs { tabs } => {
435 check_vec_len(
436 tabs,
437 MAX_SURFACE_COLUMNS,
438 "surfaces[].descriptor.root_node.tabs",
439 )?;
440 for tab in tabs {
441 check_string_len(
442 &tab.label,
443 MAX_SHORT_STRING_LEN,
444 "surfaces[].descriptor.root_node.tabs[].label",
445 )?;
446 validate_surface_node(&tab.root, depth + 1)?;
447 }
448 }
449 surfaces::SurfaceNode::Callout { text, .. } => {
450 check_string_len(
451 text,
452 MAX_MEDIUM_STRING_LEN,
453 "surfaces[].descriptor.root_node.callout",
454 )?;
455 }
456 surfaces::SurfaceNode::EmptyState { title, description } => {
457 check_string_len(
458 title,
459 MAX_SHORT_STRING_LEN,
460 "surfaces[].descriptor.root_node.empty_state.title",
461 )?;
462 check_opt_string_len(
463 description,
464 MAX_MEDIUM_STRING_LEN,
465 "surfaces[].descriptor.root_node.empty_state.description",
466 )?;
467 }
468 surfaces::SurfaceNode::ModalTrigger { modal_nodes, .. } => {
469 check_vec_len(
470 modal_nodes,
471 MAX_SURFACE_FIELDS,
472 "surfaces[].descriptor.root_node.modal_nodes",
473 )?;
474 for child in modal_nodes {
475 validate_surface_node(child, depth + 1)?;
476 }
477 }
478 surfaces::SurfaceNode::WorkflowTrigger { step_nodes, .. } => {
479 check_vec_len(
480 step_nodes,
481 MAX_SURFACE_WIZARD_STEPS,
482 "surfaces[].descriptor.root_node.step_nodes",
483 )?;
484 for child in step_nodes {
485 validate_surface_node(child, depth + 1)?;
486 }
487 }
488 _ => {
489 tracing::warn!(
490 ?node,
491 "unknown SurfaceNode variant; skipping wire validation"
492 );
493 }
494 }
495
496 Ok(())
497}
498
499fn validate_surface_interaction(
500 interaction: &surfaces::InteractionDescriptor,
501) -> Result<(), WireValidationError> {
502 check_opt_string_len(
503 &interaction.required_permission,
504 MAX_SHORT_STRING_LEN,
505 "surfaces[].interactions[].required_permission",
506 )?;
507 check_vec_len(
508 &interaction.sensitive_fields,
509 MAX_SURFACE_FIELDS,
510 "surfaces[].interactions[].sensitive_fields",
511 )?;
512 for field in &interaction.sensitive_fields {
513 check_string_len(
514 field,
515 MAX_SHORT_STRING_LEN,
516 "surfaces[].interactions[].sensitive_fields[]",
517 )?;
518 }
519
520 if let Some(confirmation) = &interaction.confirmation {
521 check_string_len(
522 &confirmation.title,
523 MAX_SHORT_STRING_LEN,
524 "surfaces[].interactions[].confirmation.title",
525 )?;
526 check_string_len(
527 &confirmation.message,
528 MAX_MEDIUM_STRING_LEN,
529 "surfaces[].interactions[].confirmation.message",
530 )?;
531 check_opt_string_len(
532 &confirmation.confirm_label,
533 MAX_SHORT_STRING_LEN,
534 "surfaces[].interactions[].confirmation.confirm_label",
535 )?;
536 check_opt_string_len(
537 &confirmation.cancel_label,
538 MAX_SHORT_STRING_LEN,
539 "surfaces[].interactions[].confirmation.cancel_label",
540 )?;
541 }
542
543 check_vec_len(
544 &interaction.workflow_steps,
545 MAX_SURFACE_WIZARD_STEPS,
546 "surfaces[].interactions[].workflow_steps",
547 )?;
548 for step in &interaction.workflow_steps {
549 check_string_len(
550 &step.step_id,
551 MAX_SHORT_STRING_LEN,
552 "surfaces[].interactions[].workflow_steps[].step_id",
553 )?;
554 }
555
556 if let surfaces::InteractionTransport::DirectBuiltInApi { operation_id } =
557 &interaction.transport
558 {
559 check_string_len(
560 operation_id.as_str(),
561 MAX_SHORT_STRING_LEN,
562 "surfaces[].interactions[].transport.operation_id",
563 )?;
564 }
565
566 if let Some(icon) = &interaction.icon {
567 surfaces::validate_icon_name(icon).map_err(|err| WireValidationError {
568 field: "surfaces[].interactions[].icon",
569 message: err.to_string(),
570 })?;
571 }
572
573 Ok(())
574}
575
576fn validate_surface_data_source(
577 data_source: &surfaces::DataSourceDescriptor,
578) -> Result<(), WireValidationError> {
579 match &data_source.kind {
580 surfaces::DataSourceKind::Static { data } => {
581 let data_len = serde_json::to_vec(data)
582 .map_err(|error| WireValidationError {
583 field: "surfaces[].data_sources[].kind.static.data",
584 message: format!("failed to serialize static data: {error}"),
585 })?
586 .len();
587 if data_len > MAX_SURFACE_PARAMS_LEN {
588 return Err(WireValidationError {
589 field: "surfaces[].data_sources[].kind.static.data",
590 message: format!(
591 "static data JSON is {data_len} bytes, max {MAX_SURFACE_PARAMS_LEN}"
592 ),
593 });
594 }
595 validate_surface_json_bounds(data, "surfaces[].data_sources[].kind.static.data")?;
596 }
597 surfaces::DataSourceKind::ControllerQuery { .. } => {}
598 surfaces::DataSourceKind::ProviderQuery { operation_id } => {
599 check_string_len(
600 operation_id,
601 MAX_SHORT_STRING_LEN,
602 "surfaces[].data_sources[].kind.provider_query.operation_id",
603 )?;
604 }
605 }
606
607 if let Some(pagination) = &data_source.pagination {
608 if pagination.default_page_size == 0 || pagination.max_page_size == 0 {
609 return Err(WireValidationError {
610 field: "surfaces[].data_sources[].pagination",
611 message: "page size values must be greater than zero".to_string(),
612 });
613 }
614 if pagination.default_page_size > pagination.max_page_size {
615 return Err(WireValidationError {
616 field: "surfaces[].data_sources[].pagination",
617 message: "default_page_size cannot exceed max_page_size".to_string(),
618 });
619 }
620 }
621
622 if let Some(sorting) = &data_source.sorting {
623 check_vec_len(
624 &sorting.sortable_fields,
625 MAX_SURFACE_COLUMNS,
626 "surfaces[].data_sources[].sorting.sortable_fields",
627 )?;
628 for field in &sorting.sortable_fields {
629 check_string_len(
630 field,
631 MAX_SHORT_STRING_LEN,
632 "surfaces[].data_sources[].sorting.sortable_fields[]",
633 )?;
634 }
635 check_opt_string_len(
636 &sorting.default_sort_field,
637 MAX_SHORT_STRING_LEN,
638 "surfaces[].data_sources[].sorting.default_sort_field",
639 )?;
640 }
641
642 if let Some(filtering) = &data_source.filtering {
643 check_vec_len(
644 &filtering.filter_fields,
645 MAX_SURFACE_COLUMNS,
646 "surfaces[].data_sources[].filtering.filter_fields",
647 )?;
648 for field in &filtering.filter_fields {
649 check_string_len(
650 field,
651 MAX_SHORT_STRING_LEN,
652 "surfaces[].data_sources[].filtering.filter_fields[]",
653 )?;
654 }
655 }
656
657 match &data_source.refresh_policy {
658 surfaces::RefreshPolicy::Manual => {}
659 surfaces::RefreshPolicy::Interval { seconds } => {
660 if *seconds == 0 {
661 return Err(WireValidationError {
662 field: "surfaces[].data_sources[].refresh_policy.interval.seconds",
663 message: "interval seconds must be greater than zero".to_string(),
664 });
665 }
666 }
667 surfaces::RefreshPolicy::Sse { .. } => {}
668 }
669
670 if let Some(empty_state) = &data_source.empty_state {
671 check_string_len(
672 &empty_state.title,
673 MAX_SHORT_STRING_LEN,
674 "surfaces[].data_sources[].empty_state.title",
675 )?;
676 check_opt_string_len(
677 &empty_state.description,
678 MAX_MEDIUM_STRING_LEN,
679 "surfaces[].data_sources[].empty_state.description",
680 )?;
681 }
682
683 Ok(())
684}
685
686impl WireValidate for surfaces::SurfaceRegistration {
687 fn wire_validate(&self) -> Result<(), WireValidationError> {
688 check_string_len(
689 &self.provider.provider_id,
690 MAX_SHORT_STRING_LEN,
691 "provider.provider_id",
692 )?;
693 check_string_len(
694 &self.provider.provider_namespace,
695 MAX_SHORT_STRING_LEN,
696 "provider.provider_namespace",
697 )?;
698 check_opt_string_len(
699 &self.effective_tenant_binding.tenant_id,
700 MAX_SHORT_STRING_LEN,
701 "effective_tenant_binding.tenant_id",
702 )?;
703 if self.effective_tenant_binding.scope == surfaces::Scope::Tenant {
704 let tenant_id =
705 self.effective_tenant_binding
706 .tenant_id
707 .as_deref()
708 .ok_or(WireValidationError {
709 field: "effective_tenant_binding.tenant_id",
710 message: "tenant scope requires tenant_id".to_string(),
711 })?;
712 uuid::Uuid::parse_str(tenant_id).map_err(|error| WireValidationError {
713 field: "effective_tenant_binding.tenant_id",
714 message: format!("invalid tenant UUID: {error}"),
715 })?;
716 } else if let Some(tenant_id) = &self.effective_tenant_binding.tenant_id {
717 uuid::Uuid::parse_str(tenant_id).map_err(|error| WireValidationError {
718 field: "effective_tenant_binding.tenant_id",
719 message: format!("invalid tenant UUID: {error}"),
720 })?;
721 }
722 check_vec_len(&self.surfaces, MAX_SURFACE_MANIFESTS, "surfaces")?;
723
724 if let Some(ref metadata) = self.encryption_metadata {
725 check_string_len(
726 &metadata.key_id,
727 MAX_SHORT_STRING_LEN,
728 "encryption_metadata.key_id",
729 )?;
730 check_string_len(
731 &metadata.public_key,
732 MAX_LONG_STRING_LEN,
733 "encryption_metadata.public_key",
734 )?;
735 }
736
737 for surface in &self.surfaces {
738 check_string_len(
739 &surface.descriptor.label,
740 MAX_SHORT_STRING_LEN,
741 "surfaces[].descriptor.label",
742 )?;
743 check_string_len(
744 &surface.descriptor.slot,
745 MAX_SHORT_STRING_LEN,
746 "surfaces[].descriptor.slot",
747 )?;
748 check_opt_string_len(
749 &surface.descriptor.required_permission,
750 MAX_SHORT_STRING_LEN,
751 "surfaces[].descriptor.required_permission",
752 )?;
753 if let Some(nav_icon) = &surface.descriptor.nav_icon {
754 surfaces::validate_icon_name(nav_icon).map_err(|err| WireValidationError {
755 field: "surfaces[].descriptor.nav_icon",
756 message: err.to_string(),
757 })?;
758 }
759 check_vec_len(
760 &surface.interactions,
761 MAX_SURFACE_ACTIONS,
762 "surfaces[].interactions",
763 )?;
764 check_vec_len(
765 &surface.data_sources,
766 MAX_SURFACE_FIELDS,
767 "surfaces[].data_sources",
768 )?;
769 validate_surface_node(&surface.descriptor.root_node, 1)?;
770 for interaction in &surface.interactions {
771 validate_surface_interaction(interaction)?;
772 }
773 for data_source in &surface.data_sources {
774 validate_surface_data_source(data_source)?;
775 }
776 }
777
778 Ok(())
779 }
780}
781
782impl WireValidate for surfaces::SurfaceActionRequest {
783 fn wire_validate(&self) -> Result<(), WireValidationError> {
784 check_string_len(&self.tenant_id, MAX_SHORT_STRING_LEN, "tenant_id")?;
785 uuid::Uuid::parse_str(&self.tenant_id).map_err(|error| WireValidationError {
786 field: "tenant_id",
787 message: format!("invalid tenant UUID: {error}"),
788 })?;
789 check_string_len(
790 &self.idempotency_key,
791 MAX_SHORT_STRING_LEN,
792 "idempotency_key",
793 )?;
794 check_opt_string_len(
795 &self.target_provider_id,
796 MAX_SHORT_STRING_LEN,
797 "target_provider_id",
798 )?;
799
800 match &self.caller_origin {
801 surfaces::CallerOrigin::UserSession {
802 user_id,
803 session_id,
804 } => {
805 check_string_len(user_id, MAX_SHORT_STRING_LEN, "caller_origin.user_id")?;
806 check_string_len(session_id, MAX_SHORT_STRING_LEN, "caller_origin.session_id")?;
807 }
808 surfaces::CallerOrigin::BuiltInSystem { principal } => {
809 check_string_len(principal, MAX_SHORT_STRING_LEN, "caller_origin.principal")?;
810 }
811 surfaces::CallerOrigin::Provider { provider_id } => {
812 check_string_len(
813 provider_id,
814 MAX_SHORT_STRING_LEN,
815 "caller_origin.provider_id",
816 )?;
817 }
818 }
819
820 let params_len = serde_json::to_vec(&self.params)
821 .map_err(|error| WireValidationError {
822 field: "params",
823 message: format!("failed to serialize params: {error}"),
824 })?
825 .len();
826 if params_len > MAX_SURFACE_PARAMS_LEN {
827 return Err(WireValidationError {
828 field: "params",
829 message: format!("params JSON is {params_len} bytes, max {MAX_SURFACE_PARAMS_LEN}"),
830 });
831 }
832 validate_surface_json_bounds(&serde_json::Value::Object(self.params.clone()), "params")?;
833
834 if let Some(ref encrypted) = self.encrypted_sensitive_params {
835 check_string_len(
836 &encrypted.key_id,
837 MAX_SHORT_STRING_LEN,
838 "encrypted_sensitive_params.key_id",
839 )?;
840 check_string_len(
841 &encrypted.ciphertext_b64,
842 MAX_LONG_STRING_LEN,
843 "encrypted_sensitive_params.ciphertext_b64",
844 )?;
845 }
846
847 Ok(())
848 }
849}
850
851impl WireValidate for surfaces::SurfaceActionCancel {
852 fn wire_validate(&self) -> Result<(), WireValidationError> {
853 check_string_len(
854 &self.target_provider_id,
855 MAX_SHORT_STRING_LEN,
856 "target_provider_id",
857 )?;
858 Ok(())
859 }
860}
861
862impl WireValidate for surfaces::SurfaceActionResponse {
863 fn wire_validate(&self) -> Result<(), WireValidationError> {
864 if let Some(ref result) = self.result {
865 let result_len = serde_json::to_vec(result)
866 .map_err(|error| WireValidationError {
867 field: "result",
868 message: format!("failed to serialize result: {error}"),
869 })?
870 .len();
871 if result_len > MAX_SURFACE_RESPONSE_LEN {
872 return Err(WireValidationError {
873 field: "result",
874 message: format!(
875 "response result is {result_len} bytes, max {MAX_SURFACE_RESPONSE_LEN}"
876 ),
877 });
878 }
879 validate_surface_json_bounds(result, "result")?;
880 }
881
882 if let Some(ref error) = self.error {
883 error.wire_validate()?;
884 }
885
886 Ok(())
887 }
888}
889
890impl WireValidate for surfaces::SurfaceActionError {
891 fn wire_validate(&self) -> Result<(), WireValidationError> {
892 check_string_len(&self.message, MAX_MEDIUM_STRING_LEN, "error.message")?;
893
894 if let Some(ref details) = self.details {
895 let details_len = serde_json::to_vec(details)
896 .map_err(|error| WireValidationError {
897 field: "error.details",
898 message: format!("failed to serialize details: {error}"),
899 })?
900 .len();
901 if details_len > MAX_SURFACE_RESPONSE_LEN {
902 return Err(WireValidationError {
903 field: "error.details",
904 message: format!(
905 "error details are {details_len} bytes, max {MAX_SURFACE_RESPONSE_LEN}"
906 ),
907 });
908 }
909 validate_surface_json_bounds(details, "error.details")?;
910 }
911
912 Ok(())
913 }
914}
915
916impl WireValidate for ReportPluginConfigPayload {
917 fn wire_validate(&self) -> Result<(), WireValidationError> {
918 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
919 check_string_len(&self.plugin_type, MAX_SHORT_STRING_LEN, "plugin_type")?;
920 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
921 let config_str = self.config.to_string();
922 check_string_len(&config_str, MAX_PLUGIN_CONFIG_JSON_LEN, "config")?;
923 Ok(())
924 }
925}
926
927impl WireValidate for ReportPluginConfigResponsePayload {
930 fn wire_validate(&self) -> Result<(), WireValidationError> {
931 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
932 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
933 Ok(())
934 }
935}
936
937impl WireValidate for CertificatePayload {
938 fn wire_validate(&self) -> Result<(), WireValidationError> {
939 check_string_len(&self.cert_pem, MAX_LONG_STRING_LEN, "cert_pem")?;
940 Ok(())
941 }
942}
943
944impl WireValidate for ErrorPayload {
945 fn wire_validate(&self) -> Result<(), WireValidationError> {
946 check_string_len(&self.message, MAX_MEDIUM_STRING_LEN, "message")?;
947 Ok(())
948 }
949}
950
951impl WireValidate for ServiceSettingsPayload {
952 fn wire_validate(&self) -> Result<(), WireValidationError> {
953 check_string_len(&self.ca_bundle_hash, MAX_SHORT_STRING_LEN, "ca_bundle_hash")?;
954 self.report_page_limits.wire_validate()?;
955 Ok(())
956 }
957}
958
959impl WireValidate for ReportPageLimits {
960 fn wire_validate(&self) -> Result<(), WireValidationError> {
961 validate_report_page_limit(
962 self.report_hosts,
963 MAX_REPORT_HOSTS as u32,
964 "report_page_limits.report_hosts",
965 )?;
966 validate_report_page_limit(
967 self.version_check_results,
968 MAX_VERSION_CHECK_RESULTS as u32,
969 "report_page_limits.version_check_results",
970 )?;
971 validate_report_page_limit(
972 self.discovery_results,
973 MAX_DISCOVERY_PLUGIN_RESULTS as u32,
974 "report_page_limits.discovery_results",
975 )?;
976 validate_report_page_limit(
977 self.batch_update_results,
978 MAX_BATCH_UPDATE_RESULTS as u32,
979 "report_page_limits.batch_update_results",
980 )?;
981 Ok(())
982 }
983}
984
985impl WireValidate for CaBundleUpdatedPayload {
986 fn wire_validate(&self) -> Result<(), WireValidationError> {
987 check_string_len(&self.ca_bundle_pem, MAX_LONG_STRING_LEN, "ca_bundle_pem")?;
988 Ok(())
989 }
990}
991
992impl WireValidate for RequestCertRenewalPayload {
993 fn wire_validate(&self) -> Result<(), WireValidationError> {
994 check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
995 Ok(())
996 }
997}
998
999impl WireValidate for ServerRestartingPayload {
1000 fn wire_validate(&self) -> Result<(), WireValidationError> {
1001 check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
1002 Ok(())
1003 }
1004}
1005
1006impl WireValidate for CheckVersionsPayload {
1007 fn wire_validate(&self) -> Result<(), WireValidationError> {
1008 check_string_len(
1009 &self.host_machine_id,
1010 MAX_SHORT_STRING_LEN,
1011 "host_machine_id",
1012 )?;
1013 check_vec_len(
1014 &self.assignments,
1015 MAX_VERSION_CHECK_ASSIGNMENTS,
1016 "assignments",
1017 )?;
1018 for assignment in &self.assignments {
1019 assignment.wire_validate()?;
1020 }
1021 Ok(())
1022 }
1023}
1024
1025impl WireValidate for VersionCheckAssignment {
1026 fn wire_validate(&self) -> Result<(), WireValidationError> {
1027 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
1028 if let Some(ref pa) = self.detect_version {
1029 pa.wire_validate()?;
1030 }
1031 if let Some(ref pa) = self.fetch_releases {
1032 pa.wire_validate()?;
1033 }
1034 Ok(())
1035 }
1036}
1037
1038impl WireValidate for PluginAssignment {
1039 fn wire_validate(&self) -> Result<(), WireValidationError> {
1040 check_string_len(
1041 &self.package_identifier,
1042 MAX_SHORT_STRING_LEN,
1043 "package_identifier",
1044 )?;
1045 Ok(())
1046 }
1047}
1048
1049impl WireValidate for ReleaseAsset {
1050 fn wire_validate(&self) -> Result<(), WireValidationError> {
1051 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "asset.name")?;
1052 check_string_len(
1053 &self.download_url,
1054 MAX_MEDIUM_STRING_LEN,
1055 "asset.download_url",
1056 )?;
1057 if let Some(ref d) = self.sha256_digest
1058 && (d.len() != SHA256_DIGEST_LEN || !d.chars().all(|c| c.is_ascii_hexdigit()))
1059 {
1060 return Err(WireValidationError {
1061 field: "asset.sha256_digest",
1062 message: format!("expected {SHA256_DIGEST_LEN} hex chars, got {}", d.len()),
1063 });
1064 }
1065 Ok(())
1066 }
1067}
1068
1069impl WireValidate for ReleaseInfo {
1070 fn wire_validate(&self) -> Result<(), WireValidationError> {
1071 check_string_len(&self.tag, MAX_SHORT_STRING_LEN, "release_info.tag")?;
1072 check_string_len(
1073 &self.release_url,
1074 MAX_MEDIUM_STRING_LEN,
1075 "release_info.release_url",
1076 )?;
1077 check_vec_len(&self.assets, MAX_RELEASE_ASSETS, "release_info.assets")?;
1078 for asset in &self.assets {
1079 asset.wire_validate()?;
1080 }
1081 Ok(())
1082 }
1083}
1084
1085impl WireValidate for ExecuteUpdatePayload {
1086 fn wire_validate(&self) -> Result<(), WireValidationError> {
1087 check_string_len(
1088 &self.host_machine_id,
1089 MAX_SHORT_STRING_LEN,
1090 "host_machine_id",
1091 )?;
1092 check_string_len(
1093 &self.software_item_name,
1094 MAX_SHORT_STRING_LEN,
1095 "software_item_name",
1096 )?;
1097 check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
1098 check_vec_len(
1099 &self.pre_update_hook_plugins,
1100 MAX_UPDATE_HOOKS,
1101 "pre_update_hook_plugins",
1102 )?;
1103 check_vec_len(
1104 &self.post_update_hook_plugins,
1105 MAX_UPDATE_HOOKS,
1106 "post_update_hook_plugins",
1107 )?;
1108 self.execute_update_plugin.wire_validate()?;
1109 if let Some(ref detect) = self.detect_version_plugin {
1110 detect.wire_validate()?;
1111 }
1112 if let Some(ref ri) = self.release_info {
1113 ri.wire_validate()?;
1114 }
1115 for plugin in &self.pre_update_hook_plugins {
1116 plugin.wire_validate()?;
1117 }
1118 for plugin in &self.post_update_hook_plugins {
1119 plugin.wire_validate()?;
1120 }
1121 Ok(())
1122 }
1123}
1124
1125impl WireValidate for ExecuteBatchUpdatePayload {
1126 fn wire_validate(&self) -> Result<(), WireValidationError> {
1127 check_string_len(
1128 &self.host_machine_id,
1129 MAX_SHORT_STRING_LEN,
1130 "host_machine_id",
1131 )?;
1132 check_vec_len(&self.updates, MAX_BATCH_UPDATES, "updates")?;
1133 check_vec_len(
1134 &self.pre_update_hook_plugins,
1135 MAX_UPDATE_HOOKS,
1136 "pre_update_hook_plugins",
1137 )?;
1138 check_vec_len(
1139 &self.post_update_hook_plugins,
1140 MAX_UPDATE_HOOKS,
1141 "post_update_hook_plugins",
1142 )?;
1143 for update in &self.updates {
1144 update.wire_validate()?;
1145 }
1146 for plugin in &self.pre_update_hook_plugins {
1147 plugin.wire_validate()?;
1148 }
1149 for plugin in &self.post_update_hook_plugins {
1150 plugin.wire_validate()?;
1151 }
1152 Ok(())
1153 }
1154}
1155
1156impl WireValidate for BatchUpdateItem {
1157 fn wire_validate(&self) -> Result<(), WireValidationError> {
1158 check_string_len(
1159 &self.package_identifier,
1160 MAX_SHORT_STRING_LEN,
1161 "package_identifier",
1162 )?;
1163 check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
1164 Ok(())
1165 }
1166}
1167
1168impl WireValidate for DiscoverSoftwarePayload {
1169 fn wire_validate(&self) -> Result<(), WireValidationError> {
1170 check_string_len(
1171 &self.host_machine_id,
1172 MAX_SHORT_STRING_LEN,
1173 "host_machine_id",
1174 )?;
1175 check_vec_len(&self.plugins, MAX_DISCOVERY_PLUGINS, "plugins")?;
1176 Ok(())
1177 }
1178}
1179
1180impl WireValidate for SetUpdateFreezePayload {
1181 fn wire_validate(&self) -> Result<(), WireValidationError> {
1182 check_opt_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
1183 Ok(())
1184 }
1185}
1186
1187impl WireValidate for UpdateStdinDataPayload {
1188 fn wire_validate(&self) -> Result<(), WireValidationError> {
1189 check_string_len(&self.data, MAX_STDIN_DATA_LEN, "data")?;
1190 Ok(())
1191 }
1192}
1193
1194impl WireValidate for StdinAttentionPayload {
1195 fn wire_validate(&self) -> Result<(), WireValidationError> {
1196 check_opt_string_len(&self.hint, MAX_MEDIUM_STRING_LEN, "hint")?;
1197 Ok(())
1198 }
1199}
1200
1201impl WireValidate for SoftwareStatesPayload {
1202 fn wire_validate(&self) -> Result<(), WireValidationError> {
1203 if self.page.total_pages < 1 {
1204 return Err(WireValidationError {
1205 field: "page.total_pages",
1206 message: "total_pages must be at least 1".to_string(),
1207 });
1208 }
1209 if self.page.page_index >= self.page.total_pages {
1210 return Err(WireValidationError {
1211 field: "page.page_index",
1212 message: format!(
1213 "page_index {} must be less than total_pages {}",
1214 self.page.page_index, self.page.total_pages
1215 ),
1216 });
1217 }
1218 check_vec_len(&self.items, MAX_SOFTWARE_STATE_ITEMS, "items")?;
1219 check_vec_len(
1220 &self.host_summaries,
1221 MAX_HOST_PACKAGE_HOST_STATES,
1222 "host_summaries",
1223 )?;
1224 check_vec_len(&self.hosts, MAX_MQTT_HOSTS, "hosts")?;
1225 for item in &self.items {
1226 item.wire_validate()?;
1227 }
1228 for host_state in &self.host_summaries {
1229 host_state.wire_validate()?;
1230 }
1231 for host in &self.hosts {
1232 host.wire_validate()?;
1233 }
1234 Ok(())
1235 }
1236}
1237
1238impl WireValidate for SoftwareStateItem {
1239 fn wire_validate(&self) -> Result<(), WireValidationError> {
1240 check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
1241 check_opt_string_len(&self.icon_url, MAX_ICON_URL_LEN, "icon_url")?;
1242 check_vec_len(&self.hosts, MAX_SOFTWARE_STATE_HOSTS, "hosts")?;
1243 for host in &self.hosts {
1244 host.wire_validate()?;
1245 }
1246 Ok(())
1247 }
1248}
1249
1250impl WireValidate for SoftwareStateHostEntry {
1251 fn wire_validate(&self) -> Result<(), WireValidationError> {
1252 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1253 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1254 check_opt_string_len(
1255 &self.installed_version,
1256 MAX_SHORT_STRING_LEN,
1257 "installed_version",
1258 )?;
1259 check_opt_string_len(&self.latest_version, MAX_SHORT_STRING_LEN, "latest_version")?;
1260 check_opt_string_len(&self.release_url, MAX_MEDIUM_STRING_LEN, "release_url")?;
1261 check_opt_string_len(&self.release_notes, MAX_LONG_STRING_LEN, "release_notes")?;
1262 check_opt_string_len(
1263 &self.update_category,
1264 MAX_SHORT_STRING_LEN,
1265 "update_category",
1266 )?;
1267 check_opt_string_len(&self.release_date, MAX_SHORT_STRING_LEN, "release_date")?;
1268 check_opt_string_len(
1269 &self.last_checked_at,
1270 MAX_SHORT_STRING_LEN,
1271 "last_checked_at",
1272 )?;
1273 Ok(())
1274 }
1275}
1276
1277impl WireValidate for HostPackageSummary {
1278 fn wire_validate(&self) -> Result<(), WireValidationError> {
1279 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1280 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1281 Ok(())
1282 }
1283}
1284
1285impl WireValidate for HostStateMetadata {
1286 fn wire_validate(&self) -> Result<(), WireValidationError> {
1287 check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1288 check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1289 check_opt_string_len(&self.os_type, MAX_SHORT_STRING_LEN, "os_type")?;
1290 check_opt_string_len(&self.os_version, MAX_SHORT_STRING_LEN, "os_version")?;
1291 check_opt_string_len(&self.architecture, MAX_SHORT_STRING_LEN, "architecture")?;
1292 check_vec_len(&self.tags, MAX_HOST_TAGS, "tags")?;
1293 for tag in &self.tags {
1294 check_string_len(tag, MAX_SHORT_STRING_LEN, "tags[]")?;
1295 }
1296 check_opt_string_len(&self.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
1297 check_opt_string_len(
1298 &self.agent_last_seen_at,
1299 MAX_SHORT_STRING_LEN,
1300 "agent_last_seen_at",
1301 )?;
1302 Ok(())
1303 }
1304}
1305
1306impl WireValidate for HostConnectivityUpdatedPayload {
1307 fn wire_validate(&self) -> Result<(), WireValidationError> {
1308 check_vec_len(&self.updates, MAX_CONNECTIVITY_UPDATES, "updates")?;
1309 for update in &self.updates {
1310 check_opt_string_len(&update.last_seen_at, MAX_SHORT_STRING_LEN, "last_seen_at")?;
1311 check_opt_string_len(&update.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
1312 }
1313 Ok(())
1314 }
1315}
1316
1317impl WireValidate for RequestCaRotationPayload {
1318 fn wire_validate(&self) -> Result<(), WireValidationError> {
1319 check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
1320 Ok(())
1321 }
1322}
1323
1324impl WireValidate for StoreServiceConfigPayload {
1327 fn wire_validate(&self) -> Result<(), WireValidationError> {
1328 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1329 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1330 let value_str = self.value.to_string();
1331 check_string_len(&value_str, MAX_SERVICE_CONFIG_VALUE_LEN, "value")?;
1332 Ok(())
1333 }
1334}
1335
1336impl WireValidate for DeleteServiceConfigPayload {
1337 fn wire_validate(&self) -> Result<(), WireValidationError> {
1338 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1339 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1340 Ok(())
1341 }
1342}
1343
1344impl WireValidate for ServiceConfigAckPayload {
1345 fn wire_validate(&self) -> Result<(), WireValidationError> {
1346 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1347 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
1348 Ok(())
1349 }
1350}
1351
1352impl WireValidate for ServiceConfigEntry {
1353 fn wire_validate(&self) -> Result<(), WireValidationError> {
1354 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1355 let value_str = self.value.to_string();
1356 check_string_len(&value_str, MAX_SERVICE_CONFIG_VALUE_LEN, "value")?;
1357 Ok(())
1358 }
1359}
1360
1361impl WireValidate for ServiceConfigKey {
1362 fn wire_validate(&self) -> Result<(), WireValidationError> {
1363 check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1364 Ok(())
1365 }
1366}
1367
1368impl WireValidate for ServiceConfigDeliveryPayload {
1369 fn wire_validate(&self) -> Result<(), WireValidationError> {
1370 check_vec_len(&self.entries, MAX_SERVICE_CONFIG_ENTRIES, "entries")?;
1371 for (i, entry) in self.entries.iter().enumerate() {
1372 entry.wire_validate().map_err(|mut e| {
1373 e.field = "entries[i]";
1374 e
1375 })?;
1376 let _ = i; }
1378 Ok(())
1379 }
1380}
1381
1382impl WireValidate for ServiceConfigUpdatedPayload {
1383 fn wire_validate(&self) -> Result<(), WireValidationError> {
1384 check_vec_len(&self.changed, MAX_SERVICE_CONFIG_ENTRIES, "changed")?;
1385 check_vec_len(&self.deleted, MAX_SERVICE_CONFIG_ENTRIES, "deleted")?;
1386 for entry in &self.changed {
1387 entry.wire_validate()?;
1388 }
1389 for key in &self.deleted {
1390 key.wire_validate()?;
1391 }
1392 Ok(())
1393 }
1394}
1395
1396impl WireValidate for WorkloadClaimPayload {
1399 fn wire_validate(&self) -> Result<(), WireValidationError> {
1400 check_map_len(&self.claims, MAX_WORKLOAD_CLAIM_KEYS, "claims")?;
1401 for key in self.claims.keys() {
1402 check_string_len(key, MAX_SHORT_STRING_LEN, "claims[key]")?;
1403 }
1404 Ok(())
1405 }
1406}
1407
1408impl WireValidate for WorkloadClaimResultPayload {
1409 fn wire_validate(&self) -> Result<(), WireValidationError> {
1410 check_set_len(&self.granted, MAX_WORKLOAD_CLAIM_KEYS, "granted")?;
1411 check_set_len(&self.rejected, MAX_WORKLOAD_CLAIM_KEYS, "rejected")?;
1412 for key in &self.granted {
1413 check_string_len(key, MAX_SHORT_STRING_LEN, "granted[key]")?;
1414 }
1415 for key in &self.rejected {
1416 check_string_len(key, MAX_SHORT_STRING_LEN, "rejected[key]")?;
1417 }
1418 Ok(())
1419 }
1420}
1421
1422impl WireValidate for WorkloadReleasePayload {
1423 fn wire_validate(&self) -> Result<(), WireValidationError> {
1424 check_set_len(&self.keys, MAX_WORKLOAD_CLAIM_KEYS, "keys")?;
1425 for key in &self.keys {
1426 check_string_len(key, MAX_SHORT_STRING_LEN, "keys[key]")?;
1427 }
1428 Ok(())
1429 }
1430}
1431
1432impl WireValidate for WorkloadClaimAnnouncementPayload {
1433 fn wire_validate(&self) -> Result<(), WireValidationError> {
1434 check_map_len(&self.claimed, MAX_WORKLOAD_CLAIM_KEYS, "claimed")?;
1435 check_set_len(&self.released, MAX_WORKLOAD_CLAIM_KEYS, "released")?;
1436 check_string_len(&self.claimed_at, MAX_SHORT_STRING_LEN, "claimed_at")?;
1437 for key in self.claimed.keys() {
1438 check_string_len(key, MAX_SHORT_STRING_LEN, "claimed[key]")?;
1439 }
1440 for key in &self.released {
1441 check_string_len(key, MAX_SHORT_STRING_LEN, "released[key]")?;
1442 }
1443 Ok(())
1444 }
1445}
1446
1447impl WireValidate for WorkloadClaimSyncResponsePayload {
1448 fn wire_validate(&self) -> Result<(), WireValidationError> {
1449 check_map_len(&self.claims, MAX_WORKLOAD_CLAIM_KEYS, "claims")?;
1450 for (key, entry) in &self.claims {
1451 check_string_len(key, MAX_SHORT_STRING_LEN, "claims[key]")?;
1452 check_string_len(
1453 &entry.claimed_at,
1454 MAX_SHORT_STRING_LEN,
1455 "claims[].claimed_at",
1456 )?;
1457 }
1458 Ok(())
1459 }
1460}
1461
1462impl WireValidate for TestPluginConfigPayload {
1465 fn wire_validate(&self) -> Result<(), WireValidationError> {
1466 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1467 check_string_len(
1468 &self.host_machine_id,
1469 MAX_SHORT_STRING_LEN,
1470 "host_machine_id",
1471 )?;
1472 check_string_len(&self.plugin_type, MAX_SHORT_STRING_LEN, "plugin_type")?;
1473 check_opt_string_len(
1474 &self.package_identifier,
1475 MAX_SHORT_STRING_LEN,
1476 "package_identifier",
1477 )?;
1478 let config_str = self.config.to_string();
1479 check_string_len(&config_str, MAX_PLUGIN_CONFIG_JSON_LEN, "config")?;
1480 Ok(())
1481 }
1482}
1483
1484impl WireValidate for TestPluginConfigResultPayload {
1485 fn wire_validate(&self) -> Result<(), WireValidationError> {
1486 check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1487 check_opt_string_len(&self.output, MAX_CONFIG_TEST_OUTPUT_LEN, "output")?;
1488 check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
1489 check_opt_string_len(
1490 &self.detected_version,
1491 MAX_SHORT_STRING_LEN,
1492 "detected_version",
1493 )?;
1494 Ok(())
1495 }
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500 #![expect(
1501 clippy::assertions_on_result_states,
1502 reason = "test assertions — assert!(result.is_ok()) are idiomatic in tests"
1503 )]
1504
1505 use super::*;
1506
1507 #[test]
1508 fn service_message_report_hosts_validates() {
1509 let msg = ServiceMessage::ReportHosts(ReportHostsPayload {
1510 hosts: vec![HostInfo {
1511 machine_id: "test-id".to_string(),
1512 os_type: Some("linux".to_string()),
1513 os_version: None,
1514 architecture: None,
1515 hostname: None,
1516 ip_address: None,
1517 agent_host_id: None,
1518 features: None,
1519 }],
1520 agent_version: "1.0.0".to_string(),
1521 capabilities: std::collections::BTreeSet::new(),
1522 });
1523 assert!(msg.wire_validate().is_ok());
1524 }
1525
1526 #[test]
1527 fn service_message_report_hosts_too_many() {
1528 let hosts: Vec<HostInfo> = (0..MAX_REPORT_HOSTS + 1)
1529 .map(|i| HostInfo {
1530 machine_id: format!("host-{i}"),
1531 os_type: None,
1532 os_version: None,
1533 architecture: None,
1534 hostname: None,
1535 ip_address: None,
1536 agent_host_id: None,
1537 features: None,
1538 })
1539 .collect();
1540 let msg = ServiceMessage::ReportHosts(ReportHostsPayload {
1541 hosts,
1542 agent_version: "1.0.0".to_string(),
1543 capabilities: std::collections::BTreeSet::new(),
1544 });
1545 let err = msg.wire_validate().unwrap_err();
1546 assert_eq!(err.field, "hosts");
1547 }
1548
1549 #[test]
1550 fn controller_message_check_versions_validates() {
1551 let msg = ControllerMessage::CheckVersions(CheckVersionsPayload {
1552 host_machine_id: "test".to_string(),
1553 assignments: vec![],
1554 });
1555 assert!(msg.wire_validate().is_ok());
1556 }
1557
1558 #[test]
1559 fn controller_message_check_versions_too_many() {
1560 let assignments: Vec<VersionCheckAssignment> = (0..MAX_VERSION_CHECK_ASSIGNMENTS + 1)
1561 .map(|i| VersionCheckAssignment {
1562 software_item_id: uuid::Uuid::nil(),
1563 name: format!("item-{i}"),
1564 detect_version: None,
1565 fetch_releases: None,
1566 host_software_item_id: None,
1567 })
1568 .collect();
1569 let msg = ControllerMessage::CheckVersions(CheckVersionsPayload {
1570 host_machine_id: "test".to_string(),
1571 assignments,
1572 });
1573 let err = msg.wire_validate().unwrap_err();
1574 assert_eq!(err.field, "assignments");
1575 }
1576
1577 #[test]
1578 fn set_update_freeze_validates() {
1579 let payload = SetUpdateFreezePayload {
1580 enabled: true,
1581 reason: Some("test".to_string()),
1582 };
1583 assert!(payload.wire_validate().is_ok());
1584 }
1585
1586 #[test]
1587 fn set_update_freeze_reason_too_long() {
1588 let payload = SetUpdateFreezePayload {
1589 enabled: true,
1590 reason: Some("x".repeat(MAX_MEDIUM_STRING_LEN + 1)),
1591 };
1592 assert!(payload.wire_validate().is_err());
1593 }
1594
1595 #[test]
1596 fn release_asset_validates() {
1597 let asset = ReleaseAsset {
1598 name: "app.tar.gz".to_string(),
1599 download_url: "https://example.com/app".to_string(),
1600 size: None,
1601 content_type: None,
1602 sha256_digest: Some("a".repeat(64)),
1603 };
1604 assert!(asset.wire_validate().is_ok());
1605 }
1606
1607 #[test]
1608 fn release_asset_invalid_digest_wrong_length() {
1609 let asset = ReleaseAsset {
1610 name: "app.tar.gz".to_string(),
1611 download_url: "https://example.com/app".to_string(),
1612 size: None,
1613 content_type: None,
1614 sha256_digest: Some("abc".to_string()),
1615 };
1616 let err = asset.wire_validate().unwrap_err();
1617 assert_eq!(err.field, "asset.sha256_digest");
1618 }
1619
1620 #[test]
1621 fn release_asset_invalid_digest_non_hex() {
1622 let asset = ReleaseAsset {
1623 name: "app.tar.gz".to_string(),
1624 download_url: "https://example.com/app".to_string(),
1625 size: None,
1626 content_type: None,
1627 sha256_digest: Some("z".repeat(64)),
1628 };
1629 let err = asset.wire_validate().unwrap_err();
1630 assert_eq!(err.field, "asset.sha256_digest");
1631 }
1632
1633 #[test]
1634 fn release_info_validates() {
1635 let info = ReleaseInfo {
1636 tag: "v1.0.0".to_string(),
1637 release_url: "https://example.com/release".to_string(),
1638 assets: vec![],
1639 attestation_status: None,
1640 require_attestation: false,
1641 };
1642 assert!(info.wire_validate().is_ok());
1643 }
1644
1645 #[test]
1646 fn release_info_too_many_assets() {
1647 let assets: Vec<ReleaseAsset> = (0..MAX_RELEASE_ASSETS + 1)
1648 .map(|i| ReleaseAsset {
1649 name: format!("asset-{i}"),
1650 download_url: format!("https://example.com/{i}"),
1651 size: None,
1652 content_type: None,
1653 sha256_digest: None,
1654 })
1655 .collect();
1656 let info = ReleaseInfo {
1657 tag: "v1.0.0".to_string(),
1658 release_url: "https://example.com".to_string(),
1659 assets,
1660 attestation_status: None,
1661 require_attestation: false,
1662 };
1663 let err = info.wire_validate().unwrap_err();
1664 assert_eq!(err.field, "release_info.assets");
1665 }
1666
1667 #[test]
1668 fn execute_update_validates() {
1669 let payload = ExecuteUpdatePayload {
1670 host_machine_id: "test".to_string(),
1671 update_history_id: uuid::Uuid::nil(),
1672 software_item_id: uuid::Uuid::nil(),
1673 software_item_name: "test".to_string(),
1674 to_version: "1.0".to_string(),
1675 detect_version_plugin: None,
1676 execute_update_plugin: PluginAssignment {
1677 plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
1678 package_identifier: "test".to_string(),
1679 config: serde_json::json!({}),
1680 },
1681 pre_update_hook_plugins: vec![],
1682 post_update_hook_plugins: vec![],
1683 release_info: Some(ReleaseInfo {
1684 tag: "v1.0".to_string(),
1685 release_url: "https://example.com".to_string(),
1686 assets: vec![],
1687 attestation_status: None,
1688 require_attestation: false,
1689 }),
1690 timeout: std::time::Duration::from_secs(60),
1691 interactive: false,
1692 };
1693 assert!(payload.wire_validate().is_ok());
1694 }
1695
1696 #[test]
1697 fn execute_update_too_many_hook_plugins() {
1698 let plugins: Vec<PluginAssignment> = (0..MAX_UPDATE_HOOKS + 1)
1699 .map(|_| PluginAssignment {
1700 plugin_type: plugin_ids::HOOK_SHELL.clone(),
1701 package_identifier: String::new(),
1702 config: serde_json::json!({}),
1703 })
1704 .collect();
1705 let payload = ExecuteUpdatePayload {
1706 host_machine_id: "test".to_string(),
1707 update_history_id: uuid::Uuid::nil(),
1708 software_item_id: uuid::Uuid::nil(),
1709 software_item_name: "test".to_string(),
1710 to_version: "1.0".to_string(),
1711 detect_version_plugin: None,
1712 execute_update_plugin: PluginAssignment {
1713 plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
1714 package_identifier: "test".to_string(),
1715 config: serde_json::json!({}),
1716 },
1717 pre_update_hook_plugins: plugins,
1718 post_update_hook_plugins: vec![],
1719 release_info: None,
1720 timeout: std::time::Duration::from_secs(60),
1721 interactive: false,
1722 };
1723 let err = payload.wire_validate().unwrap_err();
1724 assert_eq!(err.field, "pre_update_hook_plugins");
1725 }
1726
1727 #[test]
1728 fn discovery_results_validates() {
1729 let payload = DiscoveryResultsPayload {
1730 host_machine_id: "test".to_string(),
1731 results: vec![],
1732 };
1733 assert!(payload.wire_validate().is_ok());
1734 }
1735
1736 #[test]
1737 fn unknown_service_message_passes() {
1738 let msg = ServiceMessage::Unknown;
1739 assert!(msg.wire_validate().is_ok());
1740 }
1741
1742 #[test]
1743 fn unknown_controller_message_passes() {
1744 let msg = ControllerMessage::Unknown;
1745 assert!(msg.wire_validate().is_ok());
1746 }
1747
1748 #[test]
1749 fn batch_update_result_validates() {
1750 let payload = BatchUpdateResultPayload {
1751 batch_id: uuid::Uuid::nil(),
1752 results: vec![],
1753 };
1754 assert!(payload.wire_validate().is_ok());
1755 }
1756
1757 #[test]
1758 fn batch_update_result_too_many() {
1759 let results: Vec<BatchUpdateItemResult> = (0..MAX_BATCH_UPDATE_RESULTS + 1)
1760 .map(|_| BatchUpdateItemResult {
1761 host_software_item_id: uuid::Uuid::nil(),
1762 update_history_id: uuid::Uuid::nil(),
1763 status: UpdateFinalStatus::Completed,
1764 output: String::new(),
1765 installed_version: None,
1766 error: None,
1767 })
1768 .collect();
1769 let payload = BatchUpdateResultPayload {
1770 batch_id: uuid::Uuid::nil(),
1771 results,
1772 };
1773 let err = payload.wire_validate().unwrap_err();
1774 assert_eq!(err.field, "results");
1775 }
1776
1777 fn test_surface_registration() -> surfaces::SurfaceRegistration {
1780 surfaces::SurfaceRegistration {
1781 provider: surfaces::ProviderIdentity {
1782 provider_id: "uptrakit-agent-ssh".to_string(),
1783 provider_kind: surfaces::ProviderKind::Service,
1784 provider_namespace: "uptrakit.agent.ssh".to_string(),
1785 },
1786 framework_generation: surfaces::FrameworkGeneration::new(1, 0),
1787 capabilities: surfaces::CapabilitySet::default(),
1788 effective_tenant_binding: surfaces::EffectiveTenantBinding {
1789 scope: surfaces::Scope::Tenant,
1790 tenant_id: Some(uuid::Uuid::nil().to_string()),
1791 },
1792 surfaces: vec![surfaces::RegisteredSurface {
1793 descriptor: surfaces::SurfaceDescriptor::builder()
1794 .surface_id(surfaces::SurfaceId::new("ssh.guest.panel").unwrap())
1795 .label("SSH Guests")
1796 .priority(100)
1797 .slot(surfaces::SLOT_SETTINGS_TABS)
1798 .scope(surfaces::Scope::Tenant)
1799 .targeting(surfaces::Targeting::Universal)
1800 .provider_kind(surfaces::ProviderKind::Service)
1801 .required_capabilities(surfaces::CapabilitySet::default())
1802 .root_node(surfaces::SurfaceNode::section(
1803 Some("Guests".to_string()),
1804 vec![surfaces::SurfaceNode::TextBlock {
1805 text: "Guests view".to_string(),
1806 }],
1807 ))
1808 .build(),
1809 interactions: vec![surfaces::InteractionDescriptor::new(
1810 surfaces::InteractionId::new("refresh").unwrap(),
1811 surfaces::InteractionKind::MutationAction,
1812 "Refresh",
1813 surfaces::InteractionTransport::ProviderProxied,
1814 )],
1815 data_sources: vec![surfaces::DataSourceDescriptor {
1816 data_source_id: surfaces::DataSourceId::new("guest.rows").unwrap(),
1817 kind: surfaces::DataSourceKind::Static {
1818 data: serde_json::json!({"rows": []}),
1819 },
1820 result_schema: surfaces::SchemaContract::Object,
1821 pagination: None,
1822 sorting: None,
1823 filtering: None,
1824 refresh_policy: surfaces::RefreshPolicy::Manual,
1825 empty_state: None,
1826 }],
1827 }],
1828 encryption_metadata: None,
1829 }
1830 }
1831
1832 fn nested_json_array(depth: usize) -> serde_json::Value {
1833 let mut value = serde_json::json!(0);
1834 for _ in 0..depth {
1835 value = serde_json::json!([value]);
1836 }
1837 value
1838 }
1839
1840 #[test]
1841 fn surface_registration_rejects_oversized_nested_root_node_text() {
1842 let mut payload = test_surface_registration();
1843 payload.surfaces[0].descriptor.root_node = surfaces::SurfaceNode::section(
1844 None::<String>,
1845 vec![surfaces::SurfaceNode::Tabs {
1846 tabs: vec![surfaces::SurfaceTab {
1847 id: surfaces::SurfaceTabId::new("guests").unwrap(),
1848 label: "Guests".to_string(),
1849 root: surfaces::SurfaceNode::TextBlock {
1850 text: "x".repeat(MAX_MEDIUM_STRING_LEN + 1),
1851 },
1852 }],
1853 }],
1854 );
1855
1856 let err = payload.wire_validate().unwrap_err();
1857 assert_eq!(err.field, "surfaces[].descriptor.root_node.text");
1858 }
1859
1860 #[test]
1861 fn surface_registration_rejects_invalid_interaction_confirmation_text() {
1862 let mut payload = test_surface_registration();
1863 {
1864 let mut i = surfaces::InteractionDescriptor::new(
1865 surfaces::InteractionId::new("danger.refresh").unwrap(),
1866 surfaces::InteractionKind::ConfirmableAction,
1867 "Danger Refresh",
1868 surfaces::InteractionTransport::ProviderProxied,
1869 );
1870 i.confirmation = Some(surfaces::InteractionConfirmation {
1871 title: "Confirm".to_string(),
1872 message: "x".repeat(MAX_MEDIUM_STRING_LEN + 1),
1873 confirm_label: None,
1874 cancel_label: None,
1875 severity: surfaces::ConfirmationSeverity::Warning,
1876 });
1877 payload.surfaces[0].interactions[0] = i;
1878 }
1879
1880 let err = payload.wire_validate().unwrap_err();
1881 assert_eq!(err.field, "surfaces[].interactions[].confirmation.message");
1882 }
1883
1884 #[test]
1885 fn surface_registration_rejects_empty_nav_icon() {
1886 let mut payload = test_surface_registration();
1887 payload.surfaces[0].descriptor.nav_icon = Some(String::new());
1888 let err = payload.wire_validate().unwrap_err();
1889 assert_eq!(err.field, "surfaces[].descriptor.nav_icon");
1890 }
1891
1892 #[test]
1893 fn surface_registration_rejects_oversized_nav_icon() {
1894 let mut payload = test_surface_registration();
1895 payload.surfaces[0].descriptor.nav_icon =
1896 Some("x".repeat(uptrakit_surfaces::MAX_ICON_NAME_LEN + 1));
1897 let err = payload.wire_validate().unwrap_err();
1898 assert_eq!(err.field, "surfaces[].descriptor.nav_icon");
1899 }
1900
1901 #[test]
1902 fn surface_registration_accepts_valid_nav_icon() {
1903 let mut payload = test_surface_registration();
1904 payload.surfaces[0].descriptor.nav_icon = Some("package".to_string());
1905 assert!(payload.wire_validate().is_ok());
1906 }
1907
1908 #[test]
1909 fn surface_registration_rejects_pascal_case_nav_icon() {
1910 let mut payload = test_surface_registration();
1911 payload.surfaces[0].descriptor.nav_icon = Some("Package".to_string());
1912 let err = payload.wire_validate().unwrap_err();
1913 assert_eq!(err.field, "surfaces[].descriptor.nav_icon");
1914 }
1915
1916 #[test]
1917 fn surface_registration_rejects_empty_interaction_icon() {
1918 let mut payload = test_surface_registration();
1919 payload.surfaces[0].interactions[0].icon = Some(String::new());
1920 let err = payload.wire_validate().unwrap_err();
1921 assert_eq!(err.field, "surfaces[].interactions[].icon");
1922 }
1923
1924 #[test]
1925 fn surface_registration_rejects_oversized_interaction_icon() {
1926 let mut payload = test_surface_registration();
1927 payload.surfaces[0].interactions[0].icon =
1928 Some("a".repeat(uptrakit_surfaces::MAX_ICON_NAME_LEN + 1));
1929 let err = payload.wire_validate().unwrap_err();
1930 assert_eq!(err.field, "surfaces[].interactions[].icon");
1931 }
1932
1933 #[test]
1934 fn surface_registration_rejects_pascal_case_interaction_icon() {
1935 let mut payload = test_surface_registration();
1936 payload.surfaces[0].interactions[0].icon = Some("Trash2".to_string());
1937 let err = payload.wire_validate().unwrap_err();
1938 assert_eq!(err.field, "surfaces[].interactions[].icon");
1939 }
1940
1941 #[test]
1942 fn surface_registration_rejects_underscore_interaction_icon() {
1943 let mut payload = test_surface_registration();
1944 payload.surfaces[0].interactions[0].icon = Some("trash_2".to_string());
1945 let err = payload.wire_validate().unwrap_err();
1946 assert_eq!(err.field, "surfaces[].interactions[].icon");
1947 }
1948
1949 #[test]
1950 fn surface_registration_accepts_valid_interaction_icon() {
1951 let mut payload = test_surface_registration();
1952 payload.surfaces[0].interactions[0].icon = Some("trash-2".to_string());
1953 assert!(payload.wire_validate().is_ok());
1954 }
1955
1956 #[test]
1957 fn surface_registration_rejects_invalid_data_source_metadata() {
1958 let mut payload = test_surface_registration();
1959 payload.surfaces[0].data_sources[0] = surfaces::DataSourceDescriptor {
1960 data_source_id: surfaces::DataSourceId::new("guest.query").unwrap(),
1961 kind: surfaces::DataSourceKind::ProviderQuery {
1962 operation_id: "x".repeat(MAX_SHORT_STRING_LEN + 1),
1963 },
1964 result_schema: surfaces::SchemaContract::Object,
1965 pagination: Some(surfaces::DataSourcePagination {
1966 default_page_size: 100,
1967 max_page_size: 10,
1968 }),
1969 sorting: None,
1970 filtering: None,
1971 refresh_policy: surfaces::RefreshPolicy::Interval { seconds: 0 },
1972 empty_state: None,
1973 };
1974
1975 let err = payload.wire_validate().unwrap_err();
1976 assert_eq!(
1977 err.field,
1978 "surfaces[].data_sources[].kind.provider_query.operation_id"
1979 );
1980 }
1981
1982 #[test]
1983 fn surface_registration_rejects_overdeep_static_data() {
1984 let mut payload = test_surface_registration();
1985 payload.surfaces[0].data_sources[0] = surfaces::DataSourceDescriptor {
1986 data_source_id: surfaces::DataSourceId::new("guest.deep").unwrap(),
1987 kind: surfaces::DataSourceKind::Static {
1988 data: nested_json_array(MAX_SURFACE_JSON_DEPTH + 1),
1989 },
1990 result_schema: surfaces::SchemaContract::Array,
1991 pagination: None,
1992 sorting: None,
1993 filtering: None,
1994 refresh_policy: surfaces::RefreshPolicy::Manual,
1995 empty_state: None,
1996 };
1997
1998 let err = payload.wire_validate().unwrap_err();
1999 assert_eq!(err.field, "surfaces[].data_sources[].kind.static.data");
2000 }
2001
2002 #[test]
2003 fn surface_action_request_rejects_invalid_tenant_uuid() {
2004 let payload = surfaces::SurfaceActionRequest {
2005 request_id: uuid::Uuid::new_v4(),
2006 tenant_id: "not-a-uuid".to_string(),
2007 surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
2008 interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
2009 idempotency_key: "idem-1".to_string(),
2010 target_provider_id: None,
2011 caller_origin: surfaces::CallerOrigin::Provider {
2012 provider_id: "uptrakit-agent-ssh".to_string(),
2013 },
2014 params: serde_json::Map::new(),
2015 encrypted_sensitive_params: None,
2016 };
2017
2018 let err = payload.wire_validate().unwrap_err();
2019 assert_eq!(err.field, "tenant_id");
2020 }
2021
2022 #[test]
2023 fn surface_action_request_rejects_overdeep_params_json() {
2024 let payload = surfaces::SurfaceActionRequest {
2025 request_id: uuid::Uuid::new_v4(),
2026 tenant_id: uuid::Uuid::nil().to_string(),
2027 surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
2028 interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
2029 idempotency_key: "idem-1".to_string(),
2030 target_provider_id: None,
2031 caller_origin: surfaces::CallerOrigin::Provider {
2032 provider_id: "uptrakit-agent-ssh".to_string(),
2033 },
2034 params: serde_json::json!({
2035 "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
2036 })
2037 .as_object()
2038 .unwrap()
2039 .clone(),
2040 encrypted_sensitive_params: None,
2041 };
2042
2043 let err = payload.wire_validate().unwrap_err();
2044 assert_eq!(err.field, "params");
2045 }
2046
2047 #[test]
2048 fn surface_action_request_rejects_over_node_count_params_json() {
2049 let payload = surfaces::SurfaceActionRequest {
2050 request_id: uuid::Uuid::new_v4(),
2051 tenant_id: uuid::Uuid::nil().to_string(),
2052 surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
2053 interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
2054 idempotency_key: "idem-1".to_string(),
2055 target_provider_id: None,
2056 caller_origin: surfaces::CallerOrigin::Provider {
2057 provider_id: "uptrakit-agent-ssh".to_string(),
2058 },
2059 params: serde_json::json!({
2060 "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
2061 })
2062 .as_object()
2063 .unwrap()
2064 .clone(),
2065 encrypted_sensitive_params: None,
2066 };
2067
2068 let err = payload.wire_validate().unwrap_err();
2069 assert_eq!(err.field, "params");
2070 }
2071
2072 #[test]
2073 fn surface_action_response_rejects_overdeep_result_json() {
2074 let payload = surfaces::SurfaceActionResponse {
2075 request_id: uuid::Uuid::new_v4(),
2076 success: true,
2077 result: Some(serde_json::json!({
2078 "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
2079 })),
2080 error: None,
2081 };
2082
2083 let err = payload.wire_validate().unwrap_err();
2084 assert_eq!(err.field, "result");
2085 }
2086
2087 #[test]
2088 fn surface_action_response_rejects_over_node_count_result_json() {
2089 let payload = surfaces::SurfaceActionResponse {
2090 request_id: uuid::Uuid::new_v4(),
2091 success: true,
2092 result: Some(serde_json::json!({
2093 "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
2094 })),
2095 error: None,
2096 };
2097
2098 let err = payload.wire_validate().unwrap_err();
2099 assert_eq!(err.field, "result");
2100 }
2101
2102 #[test]
2103 fn surface_action_error_rejects_overdeep_details_json() {
2104 let payload = surfaces::SurfaceActionError {
2105 code: surfaces::SurfaceActionErrorCode::InternalError,
2106 message: "bad".to_string(),
2107 details: Some(serde_json::json!({
2108 "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
2109 })),
2110 };
2111
2112 let err = payload.wire_validate().unwrap_err();
2113 assert_eq!(err.field, "error.details");
2114 }
2115
2116 #[test]
2117 fn surface_action_error_rejects_over_node_count_details_json() {
2118 let payload = surfaces::SurfaceActionError {
2119 code: surfaces::SurfaceActionErrorCode::InternalError,
2120 message: "bad".to_string(),
2121 details: Some(serde_json::json!({
2122 "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
2123 })),
2124 };
2125
2126 let err = payload.wire_validate().unwrap_err();
2127 assert_eq!(err.field, "error.details");
2128 }
2129
2130 #[test]
2131 fn report_plugin_config_validates() {
2132 let msg = ServiceMessage::ReportPluginConfig(ReportPluginConfigPayload {
2133 request_id: "req-1".to_string(),
2134 plugin_type: "infrastructure_proxmox".to_string(),
2135 name: "pve.local".to_string(),
2136 config: serde_json::json!({"api_url": "https://pve:8006"}),
2137 });
2138 assert!(msg.wire_validate().is_ok());
2139 }
2140
2141 #[test]
2142 fn report_plugin_config_response_validates() {
2143 let msg =
2144 ControllerMessage::ReportPluginConfigResponse(ReportPluginConfigResponsePayload {
2145 request_id: "req-1".to_string(),
2146 success: true,
2147 plugin_config_id: Some(uuid::Uuid::nil()),
2148 error: None,
2149 });
2150 assert!(msg.wire_validate().is_ok());
2151 }
2152
2153 #[test]
2154 fn report_plugin_config_rejects_oversized_config() {
2155 let msg = ServiceMessage::ReportPluginConfig(ReportPluginConfigPayload {
2156 request_id: "req-1".to_string(),
2157 plugin_type: "infrastructure_proxmox".to_string(),
2158 name: "pve.local".to_string(),
2159 config: serde_json::Value::String("x".repeat(MAX_PLUGIN_CONFIG_JSON_LEN + 1)),
2160 });
2161 assert!(msg.wire_validate().is_err());
2162 }
2163
2164 #[test]
2165 fn update_stdin_data_validates() {
2166 let msg = ControllerMessage::UpdateStdinData(UpdateStdinDataPayload {
2167 update_history_id: uuid::Uuid::nil(),
2168 data: "aGVsbG8=".to_string(),
2169 signal: None,
2170 });
2171 assert!(msg.wire_validate().is_ok());
2172 }
2173
2174 #[test]
2175 fn update_stdin_data_rejects_oversized_data() {
2176 let msg = ControllerMessage::UpdateStdinData(UpdateStdinDataPayload {
2177 update_history_id: uuid::Uuid::nil(),
2178 data: "x".repeat(MAX_STDIN_DATA_LEN + 1),
2179 signal: None,
2180 });
2181 assert!(msg.wire_validate().is_err());
2182 }
2183
2184 #[test]
2185 fn stdin_attention_validates() {
2186 let msg = ServiceMessage::StdinAttention(StdinAttentionPayload {
2187 update_history_id: uuid::Uuid::nil(),
2188 hint: Some("waiting for config file conflict resolution".to_string()),
2189 });
2190 assert!(msg.wire_validate().is_ok());
2191 }
2192
2193 #[test]
2194 fn stdin_attention_rejects_oversized_hint() {
2195 let msg = ServiceMessage::StdinAttention(StdinAttentionPayload {
2196 update_history_id: uuid::Uuid::nil(),
2197 hint: Some("x".repeat(MAX_MEDIUM_STRING_LEN + 1)),
2198 });
2199 assert!(msg.wire_validate().is_err());
2200 }
2201
2202 #[test]
2203 fn service_settings_report_page_limits_validate() {
2204 let msg = ControllerMessage::ServiceSettings(ServiceSettingsPayload {
2205 renewal_window_hours: 6,
2206 ca_bundle_hash: "hash".to_string(),
2207 capabilities: std::collections::BTreeSet::new(),
2208 report_page_limits: ReportPageLimits::default(),
2209 shutdown_timeout: None,
2210 ping_interval: std::time::Duration::from_secs(30),
2211 tenant_id: None,
2212 trust_domain: String::new(),
2213 });
2214
2215 assert!(msg.wire_validate().is_ok());
2216 }
2217
2218 #[test]
2219 fn service_settings_reject_zero_report_page_limit() {
2220 let msg = ControllerMessage::ServiceSettings(ServiceSettingsPayload {
2221 renewal_window_hours: 6,
2222 ca_bundle_hash: "hash".to_string(),
2223 capabilities: std::collections::BTreeSet::new(),
2224 report_page_limits: ReportPageLimits {
2225 report_hosts: 0,
2226 ..ReportPageLimits::default()
2227 },
2228 shutdown_timeout: None,
2229 ping_interval: std::time::Duration::from_secs(30),
2230 tenant_id: None,
2231 trust_domain: String::new(),
2232 });
2233
2234 let err = msg.wire_validate().unwrap_err();
2235 assert_eq!(err.field, "report_page_limits.report_hosts");
2236 }
2237
2238 #[test]
2239 fn section_header_action_ids_count_exceeds_limit_is_rejected() {
2240 let mut payload = test_surface_registration();
2241 payload.surfaces[0].descriptor.root_node =
2242 surfaces::SurfaceNode::section_with_header_actions(
2243 None::<String>,
2244 vec![
2245 surfaces::InteractionId::new("a1").unwrap(),
2246 surfaces::InteractionId::new("a2").unwrap(),
2247 surfaces::InteractionId::new("a3").unwrap(),
2248 surfaces::InteractionId::new("a4").unwrap(),
2249 ],
2250 vec![],
2251 );
2252 let err = payload.wire_validate().unwrap_err();
2253 assert_eq!(
2254 err.field,
2255 "surfaces[].descriptor.root_node.header_action_ids"
2256 );
2257 assert!(err.message.contains("max 3"));
2258 }
2259
2260 #[test]
2261 fn section_header_action_ids_at_limit_is_accepted() {
2262 let mut payload = test_surface_registration();
2263 payload.surfaces[0].descriptor.root_node =
2264 surfaces::SurfaceNode::section_with_header_actions(
2265 None::<String>,
2266 vec![
2267 surfaces::InteractionId::new("a1").unwrap(),
2268 surfaces::InteractionId::new("a2").unwrap(),
2269 surfaces::InteractionId::new("a3").unwrap(),
2270 ],
2271 vec![],
2272 );
2273 assert!(payload.wire_validate().is_ok());
2274 }
2275}