Skip to main content

uptrakit_wire/
wire_validate_impls.rs

1//! `WireValidate` implementations for all wire protocol payload structs.
2//!
3//! Separated from `lib.rs` for readability. Each impl validates the struct's
4//! own fields and delegates to nested structs that also implement `WireValidate`.
5
6use 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
23// ── ServiceMessage dispatcher ─────────────────────────────────────────────────
24
25impl 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            // Forward-compatible: unknown variants from newer peers pass validation.
61            _ => {
62                tracing::debug!(
63                    "received unknown ServiceMessage variant from peer; skipping validation"
64                );
65                Ok(())
66            }
67        }
68    }
69}
70
71// ── ControllerMessage dispatcher ──────────────────────────────────────────────
72
73impl 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            // Forward-compatible: unknown variants from newer peers pass validation.
111            _ => {
112                tracing::debug!(
113                    "received unknown ControllerMessage variant from peer; skipping validation"
114                );
115                Ok(())
116            }
117        }
118    }
119}
120
121// ── ReportPagination ─────────────────────────────────────────────────────────
122
123impl 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
144// ── ServiceMessage payload impls ──────────────────────────────────────────────
145
146impl 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
927// ── ControllerMessage payload impls ───────────────────────────────────────────
928
929impl 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
1324// ── Service config store ──────────────────────────────────────────────────────
1325
1326impl 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; // avoid warning
1377        }
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
1396// ── Workload claim protocol ─────────────────────────────────────────────────
1397
1398impl 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
1462// ── Config test payload impls ────────────────────────────────────────────────
1463
1464impl 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    // ── Extension wire validation tests ─────────────────────────────────────
1778
1779    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}