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 { title, children } => {
391            check_opt_string_len(
392                title,
393                MAX_SHORT_STRING_LEN,
394                "surfaces[].descriptor.root_node.title",
395            )?;
396            check_vec_len(
397                children,
398                MAX_SURFACE_FIELDS,
399                "surfaces[].descriptor.root_node.children",
400            )?;
401            for child in children {
402                validate_surface_node(child, depth + 1)?;
403            }
404        }
405        surfaces::SurfaceNode::TextBlock { text } => {
406            check_string_len(
407                text,
408                MAX_MEDIUM_STRING_LEN,
409                "surfaces[].descriptor.root_node.text",
410            )?;
411        }
412        surfaces::SurfaceNode::KeyValue { .. } | surfaces::SurfaceNode::Table { .. } => {}
413        surfaces::SurfaceNode::Form { .. } => {}
414        surfaces::SurfaceNode::ActionBar { action_ids } => {
415            check_vec_len(
416                action_ids,
417                MAX_SURFACE_ACTION_REFS,
418                "surfaces[].descriptor.root_node.action_ids",
419            )?;
420        }
421        surfaces::SurfaceNode::Tabs { tabs } => {
422            check_vec_len(
423                tabs,
424                MAX_SURFACE_COLUMNS,
425                "surfaces[].descriptor.root_node.tabs",
426            )?;
427            for tab in tabs {
428                check_string_len(
429                    &tab.label,
430                    MAX_SHORT_STRING_LEN,
431                    "surfaces[].descriptor.root_node.tabs[].label",
432                )?;
433                validate_surface_node(&tab.root, depth + 1)?;
434            }
435        }
436        surfaces::SurfaceNode::Callout { text, .. } => {
437            check_string_len(
438                text,
439                MAX_MEDIUM_STRING_LEN,
440                "surfaces[].descriptor.root_node.callout",
441            )?;
442        }
443        surfaces::SurfaceNode::EmptyState { title, description } => {
444            check_string_len(
445                title,
446                MAX_SHORT_STRING_LEN,
447                "surfaces[].descriptor.root_node.empty_state.title",
448            )?;
449            check_opt_string_len(
450                description,
451                MAX_MEDIUM_STRING_LEN,
452                "surfaces[].descriptor.root_node.empty_state.description",
453            )?;
454        }
455        surfaces::SurfaceNode::ModalTrigger { modal_nodes, .. } => {
456            check_vec_len(
457                modal_nodes,
458                MAX_SURFACE_FIELDS,
459                "surfaces[].descriptor.root_node.modal_nodes",
460            )?;
461            for child in modal_nodes {
462                validate_surface_node(child, depth + 1)?;
463            }
464        }
465        surfaces::SurfaceNode::WorkflowTrigger { step_nodes, .. } => {
466            check_vec_len(
467                step_nodes,
468                MAX_SURFACE_WIZARD_STEPS,
469                "surfaces[].descriptor.root_node.step_nodes",
470            )?;
471            for child in step_nodes {
472                validate_surface_node(child, depth + 1)?;
473            }
474        }
475        _ => {
476            tracing::warn!(
477                ?node,
478                "unknown SurfaceNode variant; skipping wire validation"
479            );
480        }
481    }
482
483    Ok(())
484}
485
486fn validate_surface_interaction(
487    interaction: &surfaces::InteractionDescriptor,
488) -> Result<(), WireValidationError> {
489    check_opt_string_len(
490        &interaction.required_permission,
491        MAX_SHORT_STRING_LEN,
492        "surfaces[].interactions[].required_permission",
493    )?;
494    check_vec_len(
495        &interaction.sensitive_fields,
496        MAX_SURFACE_FIELDS,
497        "surfaces[].interactions[].sensitive_fields",
498    )?;
499    for field in &interaction.sensitive_fields {
500        check_string_len(
501            field,
502            MAX_SHORT_STRING_LEN,
503            "surfaces[].interactions[].sensitive_fields[]",
504        )?;
505    }
506
507    if let Some(confirmation) = &interaction.confirmation {
508        check_string_len(
509            &confirmation.title,
510            MAX_SHORT_STRING_LEN,
511            "surfaces[].interactions[].confirmation.title",
512        )?;
513        check_string_len(
514            &confirmation.message,
515            MAX_MEDIUM_STRING_LEN,
516            "surfaces[].interactions[].confirmation.message",
517        )?;
518        check_opt_string_len(
519            &confirmation.confirm_label,
520            MAX_SHORT_STRING_LEN,
521            "surfaces[].interactions[].confirmation.confirm_label",
522        )?;
523        check_opt_string_len(
524            &confirmation.cancel_label,
525            MAX_SHORT_STRING_LEN,
526            "surfaces[].interactions[].confirmation.cancel_label",
527        )?;
528    }
529
530    check_vec_len(
531        &interaction.workflow_steps,
532        MAX_SURFACE_WIZARD_STEPS,
533        "surfaces[].interactions[].workflow_steps",
534    )?;
535    for step in &interaction.workflow_steps {
536        check_string_len(
537            &step.step_id,
538            MAX_SHORT_STRING_LEN,
539            "surfaces[].interactions[].workflow_steps[].step_id",
540        )?;
541    }
542
543    if let surfaces::InteractionTransport::DirectBuiltInApi { operation_id } =
544        &interaction.transport
545    {
546        check_string_len(
547            operation_id.as_str(),
548            MAX_SHORT_STRING_LEN,
549            "surfaces[].interactions[].transport.operation_id",
550        )?;
551    }
552
553    Ok(())
554}
555
556fn validate_surface_data_source(
557    data_source: &surfaces::DataSourceDescriptor,
558) -> Result<(), WireValidationError> {
559    match &data_source.kind {
560        surfaces::DataSourceKind::Static { data } => {
561            let data_len = serde_json::to_vec(data)
562                .map_err(|error| WireValidationError {
563                    field: "surfaces[].data_sources[].kind.static.data",
564                    message: format!("failed to serialize static data: {error}"),
565                })?
566                .len();
567            if data_len > MAX_SURFACE_PARAMS_LEN {
568                return Err(WireValidationError {
569                    field: "surfaces[].data_sources[].kind.static.data",
570                    message: format!(
571                        "static data JSON is {data_len} bytes, max {MAX_SURFACE_PARAMS_LEN}"
572                    ),
573                });
574            }
575            validate_surface_json_bounds(data, "surfaces[].data_sources[].kind.static.data")?;
576        }
577        surfaces::DataSourceKind::ControllerQuery { .. } => {}
578        surfaces::DataSourceKind::ProviderQuery { operation_id } => {
579            check_string_len(
580                operation_id,
581                MAX_SHORT_STRING_LEN,
582                "surfaces[].data_sources[].kind.provider_query.operation_id",
583            )?;
584        }
585    }
586
587    if let Some(pagination) = &data_source.pagination {
588        if pagination.default_page_size == 0 || pagination.max_page_size == 0 {
589            return Err(WireValidationError {
590                field: "surfaces[].data_sources[].pagination",
591                message: "page size values must be greater than zero".to_string(),
592            });
593        }
594        if pagination.default_page_size > pagination.max_page_size {
595            return Err(WireValidationError {
596                field: "surfaces[].data_sources[].pagination",
597                message: "default_page_size cannot exceed max_page_size".to_string(),
598            });
599        }
600    }
601
602    if let Some(sorting) = &data_source.sorting {
603        check_vec_len(
604            &sorting.sortable_fields,
605            MAX_SURFACE_COLUMNS,
606            "surfaces[].data_sources[].sorting.sortable_fields",
607        )?;
608        for field in &sorting.sortable_fields {
609            check_string_len(
610                field,
611                MAX_SHORT_STRING_LEN,
612                "surfaces[].data_sources[].sorting.sortable_fields[]",
613            )?;
614        }
615        check_opt_string_len(
616            &sorting.default_sort_field,
617            MAX_SHORT_STRING_LEN,
618            "surfaces[].data_sources[].sorting.default_sort_field",
619        )?;
620    }
621
622    if let Some(filtering) = &data_source.filtering {
623        check_vec_len(
624            &filtering.filter_fields,
625            MAX_SURFACE_COLUMNS,
626            "surfaces[].data_sources[].filtering.filter_fields",
627        )?;
628        for field in &filtering.filter_fields {
629            check_string_len(
630                field,
631                MAX_SHORT_STRING_LEN,
632                "surfaces[].data_sources[].filtering.filter_fields[]",
633            )?;
634        }
635    }
636
637    match &data_source.refresh_policy {
638        surfaces::RefreshPolicy::Manual => {}
639        surfaces::RefreshPolicy::Interval { seconds } => {
640            if *seconds == 0 {
641                return Err(WireValidationError {
642                    field: "surfaces[].data_sources[].refresh_policy.interval.seconds",
643                    message: "interval seconds must be greater than zero".to_string(),
644                });
645            }
646        }
647        surfaces::RefreshPolicy::Sse { .. } => {}
648    }
649
650    if let Some(empty_state) = &data_source.empty_state {
651        check_string_len(
652            &empty_state.title,
653            MAX_SHORT_STRING_LEN,
654            "surfaces[].data_sources[].empty_state.title",
655        )?;
656        check_opt_string_len(
657            &empty_state.description,
658            MAX_MEDIUM_STRING_LEN,
659            "surfaces[].data_sources[].empty_state.description",
660        )?;
661    }
662
663    Ok(())
664}
665
666impl WireValidate for surfaces::SurfaceRegistration {
667    fn wire_validate(&self) -> Result<(), WireValidationError> {
668        check_string_len(
669            &self.provider.provider_id,
670            MAX_SHORT_STRING_LEN,
671            "provider.provider_id",
672        )?;
673        check_string_len(
674            &self.provider.provider_namespace,
675            MAX_SHORT_STRING_LEN,
676            "provider.provider_namespace",
677        )?;
678        check_opt_string_len(
679            &self.effective_tenant_binding.tenant_id,
680            MAX_SHORT_STRING_LEN,
681            "effective_tenant_binding.tenant_id",
682        )?;
683        if self.effective_tenant_binding.scope == surfaces::Scope::Tenant {
684            let tenant_id =
685                self.effective_tenant_binding
686                    .tenant_id
687                    .as_deref()
688                    .ok_or(WireValidationError {
689                        field: "effective_tenant_binding.tenant_id",
690                        message: "tenant scope requires tenant_id".to_string(),
691                    })?;
692            uuid::Uuid::parse_str(tenant_id).map_err(|error| WireValidationError {
693                field: "effective_tenant_binding.tenant_id",
694                message: format!("invalid tenant UUID: {error}"),
695            })?;
696        } else if let Some(tenant_id) = &self.effective_tenant_binding.tenant_id {
697            uuid::Uuid::parse_str(tenant_id).map_err(|error| WireValidationError {
698                field: "effective_tenant_binding.tenant_id",
699                message: format!("invalid tenant UUID: {error}"),
700            })?;
701        }
702        check_vec_len(&self.surfaces, MAX_SURFACE_MANIFESTS, "surfaces")?;
703
704        if let Some(ref metadata) = self.encryption_metadata {
705            check_string_len(
706                &metadata.key_id,
707                MAX_SHORT_STRING_LEN,
708                "encryption_metadata.key_id",
709            )?;
710            check_string_len(
711                &metadata.public_key,
712                MAX_LONG_STRING_LEN,
713                "encryption_metadata.public_key",
714            )?;
715        }
716
717        for surface in &self.surfaces {
718            check_string_len(
719                &surface.descriptor.label,
720                MAX_SHORT_STRING_LEN,
721                "surfaces[].descriptor.label",
722            )?;
723            check_string_len(
724                &surface.descriptor.slot,
725                MAX_SHORT_STRING_LEN,
726                "surfaces[].descriptor.slot",
727            )?;
728            check_opt_string_len(
729                &surface.descriptor.required_permission,
730                MAX_SHORT_STRING_LEN,
731                "surfaces[].descriptor.required_permission",
732            )?;
733            check_vec_len(
734                &surface.interactions,
735                MAX_SURFACE_ACTIONS,
736                "surfaces[].interactions",
737            )?;
738            check_vec_len(
739                &surface.data_sources,
740                MAX_SURFACE_FIELDS,
741                "surfaces[].data_sources",
742            )?;
743            validate_surface_node(&surface.descriptor.root_node, 1)?;
744            for interaction in &surface.interactions {
745                validate_surface_interaction(interaction)?;
746            }
747            for data_source in &surface.data_sources {
748                validate_surface_data_source(data_source)?;
749            }
750        }
751
752        Ok(())
753    }
754}
755
756impl WireValidate for surfaces::SurfaceActionRequest {
757    fn wire_validate(&self) -> Result<(), WireValidationError> {
758        check_string_len(&self.tenant_id, MAX_SHORT_STRING_LEN, "tenant_id")?;
759        uuid::Uuid::parse_str(&self.tenant_id).map_err(|error| WireValidationError {
760            field: "tenant_id",
761            message: format!("invalid tenant UUID: {error}"),
762        })?;
763        check_string_len(
764            &self.idempotency_key,
765            MAX_SHORT_STRING_LEN,
766            "idempotency_key",
767        )?;
768        check_opt_string_len(
769            &self.target_provider_id,
770            MAX_SHORT_STRING_LEN,
771            "target_provider_id",
772        )?;
773
774        match &self.caller_origin {
775            surfaces::CallerOrigin::UserSession {
776                user_id,
777                session_id,
778            } => {
779                check_string_len(user_id, MAX_SHORT_STRING_LEN, "caller_origin.user_id")?;
780                check_string_len(session_id, MAX_SHORT_STRING_LEN, "caller_origin.session_id")?;
781            }
782            surfaces::CallerOrigin::BuiltInSystem { principal } => {
783                check_string_len(principal, MAX_SHORT_STRING_LEN, "caller_origin.principal")?;
784            }
785            surfaces::CallerOrigin::Provider { provider_id } => {
786                check_string_len(
787                    provider_id,
788                    MAX_SHORT_STRING_LEN,
789                    "caller_origin.provider_id",
790                )?;
791            }
792        }
793
794        let params_len = serde_json::to_vec(&self.params)
795            .map_err(|error| WireValidationError {
796                field: "params",
797                message: format!("failed to serialize params: {error}"),
798            })?
799            .len();
800        if params_len > MAX_SURFACE_PARAMS_LEN {
801            return Err(WireValidationError {
802                field: "params",
803                message: format!("params JSON is {params_len} bytes, max {MAX_SURFACE_PARAMS_LEN}"),
804            });
805        }
806        validate_surface_json_bounds(&serde_json::Value::Object(self.params.clone()), "params")?;
807
808        if let Some(ref encrypted) = self.encrypted_sensitive_params {
809            check_string_len(
810                &encrypted.key_id,
811                MAX_SHORT_STRING_LEN,
812                "encrypted_sensitive_params.key_id",
813            )?;
814            check_string_len(
815                &encrypted.ciphertext_b64,
816                MAX_LONG_STRING_LEN,
817                "encrypted_sensitive_params.ciphertext_b64",
818            )?;
819        }
820
821        Ok(())
822    }
823}
824
825impl WireValidate for surfaces::SurfaceActionCancel {
826    fn wire_validate(&self) -> Result<(), WireValidationError> {
827        check_string_len(
828            &self.target_provider_id,
829            MAX_SHORT_STRING_LEN,
830            "target_provider_id",
831        )?;
832        Ok(())
833    }
834}
835
836impl WireValidate for surfaces::SurfaceActionResponse {
837    fn wire_validate(&self) -> Result<(), WireValidationError> {
838        if let Some(ref result) = self.result {
839            let result_len = serde_json::to_vec(result)
840                .map_err(|error| WireValidationError {
841                    field: "result",
842                    message: format!("failed to serialize result: {error}"),
843                })?
844                .len();
845            if result_len > MAX_SURFACE_RESPONSE_LEN {
846                return Err(WireValidationError {
847                    field: "result",
848                    message: format!(
849                        "response result is {result_len} bytes, max {MAX_SURFACE_RESPONSE_LEN}"
850                    ),
851                });
852            }
853            validate_surface_json_bounds(result, "result")?;
854        }
855
856        if let Some(ref error) = self.error {
857            error.wire_validate()?;
858        }
859
860        Ok(())
861    }
862}
863
864impl WireValidate for surfaces::SurfaceActionError {
865    fn wire_validate(&self) -> Result<(), WireValidationError> {
866        check_string_len(&self.message, MAX_MEDIUM_STRING_LEN, "error.message")?;
867
868        if let Some(ref details) = self.details {
869            let details_len = serde_json::to_vec(details)
870                .map_err(|error| WireValidationError {
871                    field: "error.details",
872                    message: format!("failed to serialize details: {error}"),
873                })?
874                .len();
875            if details_len > MAX_SURFACE_RESPONSE_LEN {
876                return Err(WireValidationError {
877                    field: "error.details",
878                    message: format!(
879                        "error details are {details_len} bytes, max {MAX_SURFACE_RESPONSE_LEN}"
880                    ),
881                });
882            }
883            validate_surface_json_bounds(details, "error.details")?;
884        }
885
886        Ok(())
887    }
888}
889
890impl WireValidate for ReportPluginConfigPayload {
891    fn wire_validate(&self) -> Result<(), WireValidationError> {
892        check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
893        check_string_len(&self.plugin_type, MAX_SHORT_STRING_LEN, "plugin_type")?;
894        check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
895        let config_str = self.config.to_string();
896        check_string_len(&config_str, MAX_PLUGIN_CONFIG_JSON_LEN, "config")?;
897        Ok(())
898    }
899}
900
901// ── ControllerMessage payload impls ───────────────────────────────────────────
902
903impl WireValidate for ReportPluginConfigResponsePayload {
904    fn wire_validate(&self) -> Result<(), WireValidationError> {
905        check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
906        check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
907        Ok(())
908    }
909}
910
911impl WireValidate for CertificatePayload {
912    fn wire_validate(&self) -> Result<(), WireValidationError> {
913        check_string_len(&self.cert_pem, MAX_LONG_STRING_LEN, "cert_pem")?;
914        Ok(())
915    }
916}
917
918impl WireValidate for ErrorPayload {
919    fn wire_validate(&self) -> Result<(), WireValidationError> {
920        check_string_len(&self.message, MAX_MEDIUM_STRING_LEN, "message")?;
921        Ok(())
922    }
923}
924
925impl WireValidate for ServiceSettingsPayload {
926    fn wire_validate(&self) -> Result<(), WireValidationError> {
927        check_string_len(&self.ca_bundle_hash, MAX_SHORT_STRING_LEN, "ca_bundle_hash")?;
928        self.report_page_limits.wire_validate()?;
929        Ok(())
930    }
931}
932
933impl WireValidate for ReportPageLimits {
934    fn wire_validate(&self) -> Result<(), WireValidationError> {
935        validate_report_page_limit(
936            self.report_hosts,
937            MAX_REPORT_HOSTS as u32,
938            "report_page_limits.report_hosts",
939        )?;
940        validate_report_page_limit(
941            self.version_check_results,
942            MAX_VERSION_CHECK_RESULTS as u32,
943            "report_page_limits.version_check_results",
944        )?;
945        validate_report_page_limit(
946            self.discovery_results,
947            MAX_DISCOVERY_PLUGIN_RESULTS as u32,
948            "report_page_limits.discovery_results",
949        )?;
950        validate_report_page_limit(
951            self.batch_update_results,
952            MAX_BATCH_UPDATE_RESULTS as u32,
953            "report_page_limits.batch_update_results",
954        )?;
955        Ok(())
956    }
957}
958
959impl WireValidate for CaBundleUpdatedPayload {
960    fn wire_validate(&self) -> Result<(), WireValidationError> {
961        check_string_len(&self.ca_bundle_pem, MAX_LONG_STRING_LEN, "ca_bundle_pem")?;
962        Ok(())
963    }
964}
965
966impl WireValidate for RequestCertRenewalPayload {
967    fn wire_validate(&self) -> Result<(), WireValidationError> {
968        check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
969        Ok(())
970    }
971}
972
973impl WireValidate for ServerRestartingPayload {
974    fn wire_validate(&self) -> Result<(), WireValidationError> {
975        check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
976        Ok(())
977    }
978}
979
980impl WireValidate for CheckVersionsPayload {
981    fn wire_validate(&self) -> Result<(), WireValidationError> {
982        check_string_len(
983            &self.host_machine_id,
984            MAX_SHORT_STRING_LEN,
985            "host_machine_id",
986        )?;
987        check_vec_len(
988            &self.assignments,
989            MAX_VERSION_CHECK_ASSIGNMENTS,
990            "assignments",
991        )?;
992        for assignment in &self.assignments {
993            assignment.wire_validate()?;
994        }
995        Ok(())
996    }
997}
998
999impl WireValidate for VersionCheckAssignment {
1000    fn wire_validate(&self) -> Result<(), WireValidationError> {
1001        check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
1002        if let Some(ref pa) = self.detect_version {
1003            pa.wire_validate()?;
1004        }
1005        if let Some(ref pa) = self.fetch_releases {
1006            pa.wire_validate()?;
1007        }
1008        Ok(())
1009    }
1010}
1011
1012impl WireValidate for PluginAssignment {
1013    fn wire_validate(&self) -> Result<(), WireValidationError> {
1014        check_string_len(
1015            &self.package_identifier,
1016            MAX_SHORT_STRING_LEN,
1017            "package_identifier",
1018        )?;
1019        Ok(())
1020    }
1021}
1022
1023impl WireValidate for ReleaseAsset {
1024    fn wire_validate(&self) -> Result<(), WireValidationError> {
1025        check_string_len(&self.name, MAX_SHORT_STRING_LEN, "asset.name")?;
1026        check_string_len(
1027            &self.download_url,
1028            MAX_MEDIUM_STRING_LEN,
1029            "asset.download_url",
1030        )?;
1031        if let Some(ref d) = self.sha256_digest
1032            && (d.len() != SHA256_DIGEST_LEN || !d.chars().all(|c| c.is_ascii_hexdigit()))
1033        {
1034            return Err(WireValidationError {
1035                field: "asset.sha256_digest",
1036                message: format!("expected {SHA256_DIGEST_LEN} hex chars, got {}", d.len()),
1037            });
1038        }
1039        Ok(())
1040    }
1041}
1042
1043impl WireValidate for ReleaseInfo {
1044    fn wire_validate(&self) -> Result<(), WireValidationError> {
1045        check_string_len(&self.tag, MAX_SHORT_STRING_LEN, "release_info.tag")?;
1046        check_string_len(
1047            &self.release_url,
1048            MAX_MEDIUM_STRING_LEN,
1049            "release_info.release_url",
1050        )?;
1051        check_vec_len(&self.assets, MAX_RELEASE_ASSETS, "release_info.assets")?;
1052        for asset in &self.assets {
1053            asset.wire_validate()?;
1054        }
1055        Ok(())
1056    }
1057}
1058
1059impl WireValidate for ExecuteUpdatePayload {
1060    fn wire_validate(&self) -> Result<(), WireValidationError> {
1061        check_string_len(
1062            &self.host_machine_id,
1063            MAX_SHORT_STRING_LEN,
1064            "host_machine_id",
1065        )?;
1066        check_string_len(
1067            &self.software_item_name,
1068            MAX_SHORT_STRING_LEN,
1069            "software_item_name",
1070        )?;
1071        check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
1072        check_vec_len(
1073            &self.pre_update_hook_plugins,
1074            MAX_UPDATE_HOOKS,
1075            "pre_update_hook_plugins",
1076        )?;
1077        check_vec_len(
1078            &self.post_update_hook_plugins,
1079            MAX_UPDATE_HOOKS,
1080            "post_update_hook_plugins",
1081        )?;
1082        self.execute_update_plugin.wire_validate()?;
1083        if let Some(ref detect) = self.detect_version_plugin {
1084            detect.wire_validate()?;
1085        }
1086        if let Some(ref ri) = self.release_info {
1087            ri.wire_validate()?;
1088        }
1089        for plugin in &self.pre_update_hook_plugins {
1090            plugin.wire_validate()?;
1091        }
1092        for plugin in &self.post_update_hook_plugins {
1093            plugin.wire_validate()?;
1094        }
1095        Ok(())
1096    }
1097}
1098
1099impl WireValidate for ExecuteBatchUpdatePayload {
1100    fn wire_validate(&self) -> Result<(), WireValidationError> {
1101        check_string_len(
1102            &self.host_machine_id,
1103            MAX_SHORT_STRING_LEN,
1104            "host_machine_id",
1105        )?;
1106        check_vec_len(&self.updates, MAX_BATCH_UPDATES, "updates")?;
1107        check_vec_len(
1108            &self.pre_update_hook_plugins,
1109            MAX_UPDATE_HOOKS,
1110            "pre_update_hook_plugins",
1111        )?;
1112        check_vec_len(
1113            &self.post_update_hook_plugins,
1114            MAX_UPDATE_HOOKS,
1115            "post_update_hook_plugins",
1116        )?;
1117        for update in &self.updates {
1118            update.wire_validate()?;
1119        }
1120        for plugin in &self.pre_update_hook_plugins {
1121            plugin.wire_validate()?;
1122        }
1123        for plugin in &self.post_update_hook_plugins {
1124            plugin.wire_validate()?;
1125        }
1126        Ok(())
1127    }
1128}
1129
1130impl WireValidate for BatchUpdateItem {
1131    fn wire_validate(&self) -> Result<(), WireValidationError> {
1132        check_string_len(
1133            &self.package_identifier,
1134            MAX_SHORT_STRING_LEN,
1135            "package_identifier",
1136        )?;
1137        check_string_len(&self.to_version, MAX_SHORT_STRING_LEN, "to_version")?;
1138        Ok(())
1139    }
1140}
1141
1142impl WireValidate for DiscoverSoftwarePayload {
1143    fn wire_validate(&self) -> Result<(), WireValidationError> {
1144        check_string_len(
1145            &self.host_machine_id,
1146            MAX_SHORT_STRING_LEN,
1147            "host_machine_id",
1148        )?;
1149        check_vec_len(&self.plugins, MAX_DISCOVERY_PLUGINS, "plugins")?;
1150        Ok(())
1151    }
1152}
1153
1154impl WireValidate for SetUpdateFreezePayload {
1155    fn wire_validate(&self) -> Result<(), WireValidationError> {
1156        check_opt_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
1157        Ok(())
1158    }
1159}
1160
1161impl WireValidate for UpdateStdinDataPayload {
1162    fn wire_validate(&self) -> Result<(), WireValidationError> {
1163        check_string_len(&self.data, MAX_STDIN_DATA_LEN, "data")?;
1164        Ok(())
1165    }
1166}
1167
1168impl WireValidate for StdinAttentionPayload {
1169    fn wire_validate(&self) -> Result<(), WireValidationError> {
1170        check_opt_string_len(&self.hint, MAX_MEDIUM_STRING_LEN, "hint")?;
1171        Ok(())
1172    }
1173}
1174
1175impl WireValidate for SoftwareStatesPayload {
1176    fn wire_validate(&self) -> Result<(), WireValidationError> {
1177        if self.page.total_pages < 1 {
1178            return Err(WireValidationError {
1179                field: "page.total_pages",
1180                message: "total_pages must be at least 1".to_string(),
1181            });
1182        }
1183        if self.page.page_index >= self.page.total_pages {
1184            return Err(WireValidationError {
1185                field: "page.page_index",
1186                message: format!(
1187                    "page_index {} must be less than total_pages {}",
1188                    self.page.page_index, self.page.total_pages
1189                ),
1190            });
1191        }
1192        check_vec_len(&self.items, MAX_SOFTWARE_STATE_ITEMS, "items")?;
1193        check_vec_len(
1194            &self.host_summaries,
1195            MAX_HOST_PACKAGE_HOST_STATES,
1196            "host_summaries",
1197        )?;
1198        check_vec_len(&self.hosts, MAX_MQTT_HOSTS, "hosts")?;
1199        for item in &self.items {
1200            item.wire_validate()?;
1201        }
1202        for host_state in &self.host_summaries {
1203            host_state.wire_validate()?;
1204        }
1205        for host in &self.hosts {
1206            host.wire_validate()?;
1207        }
1208        Ok(())
1209    }
1210}
1211
1212impl WireValidate for SoftwareStateItem {
1213    fn wire_validate(&self) -> Result<(), WireValidationError> {
1214        check_string_len(&self.name, MAX_SHORT_STRING_LEN, "name")?;
1215        check_opt_string_len(&self.icon_url, MAX_ICON_URL_LEN, "icon_url")?;
1216        check_vec_len(&self.hosts, MAX_SOFTWARE_STATE_HOSTS, "hosts")?;
1217        for host in &self.hosts {
1218            host.wire_validate()?;
1219        }
1220        Ok(())
1221    }
1222}
1223
1224impl WireValidate for SoftwareStateHostEntry {
1225    fn wire_validate(&self) -> Result<(), WireValidationError> {
1226        check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1227        check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1228        check_opt_string_len(
1229            &self.installed_version,
1230            MAX_SHORT_STRING_LEN,
1231            "installed_version",
1232        )?;
1233        check_opt_string_len(&self.latest_version, MAX_SHORT_STRING_LEN, "latest_version")?;
1234        check_opt_string_len(&self.release_url, MAX_MEDIUM_STRING_LEN, "release_url")?;
1235        check_opt_string_len(&self.release_notes, MAX_LONG_STRING_LEN, "release_notes")?;
1236        check_opt_string_len(
1237            &self.update_category,
1238            MAX_SHORT_STRING_LEN,
1239            "update_category",
1240        )?;
1241        check_opt_string_len(&self.release_date, MAX_SHORT_STRING_LEN, "release_date")?;
1242        check_opt_string_len(
1243            &self.last_checked_at,
1244            MAX_SHORT_STRING_LEN,
1245            "last_checked_at",
1246        )?;
1247        Ok(())
1248    }
1249}
1250
1251impl WireValidate for HostPackageSummary {
1252    fn wire_validate(&self) -> Result<(), WireValidationError> {
1253        check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1254        check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1255        Ok(())
1256    }
1257}
1258
1259impl WireValidate for HostStateMetadata {
1260    fn wire_validate(&self) -> Result<(), WireValidationError> {
1261        check_string_len(&self.hostname, MAX_SHORT_STRING_LEN, "hostname")?;
1262        check_string_len(&self.friendly_name, MAX_SHORT_STRING_LEN, "friendly_name")?;
1263        check_opt_string_len(&self.os_type, MAX_SHORT_STRING_LEN, "os_type")?;
1264        check_opt_string_len(&self.os_version, MAX_SHORT_STRING_LEN, "os_version")?;
1265        check_opt_string_len(&self.architecture, MAX_SHORT_STRING_LEN, "architecture")?;
1266        check_vec_len(&self.tags, MAX_HOST_TAGS, "tags")?;
1267        for tag in &self.tags {
1268            check_string_len(tag, MAX_SHORT_STRING_LEN, "tags[]")?;
1269        }
1270        check_opt_string_len(&self.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
1271        check_opt_string_len(
1272            &self.agent_last_seen_at,
1273            MAX_SHORT_STRING_LEN,
1274            "agent_last_seen_at",
1275        )?;
1276        Ok(())
1277    }
1278}
1279
1280impl WireValidate for HostConnectivityUpdatedPayload {
1281    fn wire_validate(&self) -> Result<(), WireValidationError> {
1282        check_vec_len(&self.updates, MAX_CONNECTIVITY_UPDATES, "updates")?;
1283        for update in &self.updates {
1284            check_opt_string_len(&update.last_seen_at, MAX_SHORT_STRING_LEN, "last_seen_at")?;
1285            check_opt_string_len(&update.agent_version, MAX_SHORT_STRING_LEN, "agent_version")?;
1286        }
1287        Ok(())
1288    }
1289}
1290
1291impl WireValidate for RequestCaRotationPayload {
1292    fn wire_validate(&self) -> Result<(), WireValidationError> {
1293        check_string_len(&self.reason, MAX_MEDIUM_STRING_LEN, "reason")?;
1294        Ok(())
1295    }
1296}
1297
1298// ── Service config store ──────────────────────────────────────────────────────
1299
1300impl WireValidate for StoreServiceConfigPayload {
1301    fn wire_validate(&self) -> Result<(), WireValidationError> {
1302        check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1303        check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1304        let value_str = self.value.to_string();
1305        check_string_len(&value_str, MAX_SERVICE_CONFIG_VALUE_LEN, "value")?;
1306        Ok(())
1307    }
1308}
1309
1310impl WireValidate for DeleteServiceConfigPayload {
1311    fn wire_validate(&self) -> Result<(), WireValidationError> {
1312        check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1313        check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1314        Ok(())
1315    }
1316}
1317
1318impl WireValidate for ServiceConfigAckPayload {
1319    fn wire_validate(&self) -> Result<(), WireValidationError> {
1320        check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1321        check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
1322        Ok(())
1323    }
1324}
1325
1326impl WireValidate for ServiceConfigEntry {
1327    fn wire_validate(&self) -> Result<(), WireValidationError> {
1328        check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1329        let value_str = self.value.to_string();
1330        check_string_len(&value_str, MAX_SERVICE_CONFIG_VALUE_LEN, "value")?;
1331        Ok(())
1332    }
1333}
1334
1335impl WireValidate for ServiceConfigKey {
1336    fn wire_validate(&self) -> Result<(), WireValidationError> {
1337        check_string_len(&self.key, MAX_SHORT_STRING_LEN, "key")?;
1338        Ok(())
1339    }
1340}
1341
1342impl WireValidate for ServiceConfigDeliveryPayload {
1343    fn wire_validate(&self) -> Result<(), WireValidationError> {
1344        check_vec_len(&self.entries, MAX_SERVICE_CONFIG_ENTRIES, "entries")?;
1345        for (i, entry) in self.entries.iter().enumerate() {
1346            entry.wire_validate().map_err(|mut e| {
1347                e.field = "entries[i]";
1348                e
1349            })?;
1350            let _ = i; // avoid warning
1351        }
1352        Ok(())
1353    }
1354}
1355
1356impl WireValidate for ServiceConfigUpdatedPayload {
1357    fn wire_validate(&self) -> Result<(), WireValidationError> {
1358        check_vec_len(&self.changed, MAX_SERVICE_CONFIG_ENTRIES, "changed")?;
1359        check_vec_len(&self.deleted, MAX_SERVICE_CONFIG_ENTRIES, "deleted")?;
1360        for entry in &self.changed {
1361            entry.wire_validate()?;
1362        }
1363        for key in &self.deleted {
1364            key.wire_validate()?;
1365        }
1366        Ok(())
1367    }
1368}
1369
1370// ── Workload claim protocol ─────────────────────────────────────────────────
1371
1372impl WireValidate for WorkloadClaimPayload {
1373    fn wire_validate(&self) -> Result<(), WireValidationError> {
1374        check_map_len(&self.claims, MAX_WORKLOAD_CLAIM_KEYS, "claims")?;
1375        for key in self.claims.keys() {
1376            check_string_len(key, MAX_SHORT_STRING_LEN, "claims[key]")?;
1377        }
1378        Ok(())
1379    }
1380}
1381
1382impl WireValidate for WorkloadClaimResultPayload {
1383    fn wire_validate(&self) -> Result<(), WireValidationError> {
1384        check_set_len(&self.granted, MAX_WORKLOAD_CLAIM_KEYS, "granted")?;
1385        check_set_len(&self.rejected, MAX_WORKLOAD_CLAIM_KEYS, "rejected")?;
1386        for key in &self.granted {
1387            check_string_len(key, MAX_SHORT_STRING_LEN, "granted[key]")?;
1388        }
1389        for key in &self.rejected {
1390            check_string_len(key, MAX_SHORT_STRING_LEN, "rejected[key]")?;
1391        }
1392        Ok(())
1393    }
1394}
1395
1396impl WireValidate for WorkloadReleasePayload {
1397    fn wire_validate(&self) -> Result<(), WireValidationError> {
1398        check_set_len(&self.keys, MAX_WORKLOAD_CLAIM_KEYS, "keys")?;
1399        for key in &self.keys {
1400            check_string_len(key, MAX_SHORT_STRING_LEN, "keys[key]")?;
1401        }
1402        Ok(())
1403    }
1404}
1405
1406impl WireValidate for WorkloadClaimAnnouncementPayload {
1407    fn wire_validate(&self) -> Result<(), WireValidationError> {
1408        check_map_len(&self.claimed, MAX_WORKLOAD_CLAIM_KEYS, "claimed")?;
1409        check_set_len(&self.released, MAX_WORKLOAD_CLAIM_KEYS, "released")?;
1410        check_string_len(&self.claimed_at, MAX_SHORT_STRING_LEN, "claimed_at")?;
1411        for key in self.claimed.keys() {
1412            check_string_len(key, MAX_SHORT_STRING_LEN, "claimed[key]")?;
1413        }
1414        for key in &self.released {
1415            check_string_len(key, MAX_SHORT_STRING_LEN, "released[key]")?;
1416        }
1417        Ok(())
1418    }
1419}
1420
1421impl WireValidate for WorkloadClaimSyncResponsePayload {
1422    fn wire_validate(&self) -> Result<(), WireValidationError> {
1423        check_map_len(&self.claims, MAX_WORKLOAD_CLAIM_KEYS, "claims")?;
1424        for (key, entry) in &self.claims {
1425            check_string_len(key, MAX_SHORT_STRING_LEN, "claims[key]")?;
1426            check_string_len(
1427                &entry.claimed_at,
1428                MAX_SHORT_STRING_LEN,
1429                "claims[].claimed_at",
1430            )?;
1431        }
1432        Ok(())
1433    }
1434}
1435
1436// ── Config test payload impls ────────────────────────────────────────────────
1437
1438impl WireValidate for TestPluginConfigPayload {
1439    fn wire_validate(&self) -> Result<(), WireValidationError> {
1440        check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1441        check_string_len(
1442            &self.host_machine_id,
1443            MAX_SHORT_STRING_LEN,
1444            "host_machine_id",
1445        )?;
1446        check_string_len(&self.plugin_type, MAX_SHORT_STRING_LEN, "plugin_type")?;
1447        check_opt_string_len(
1448            &self.package_identifier,
1449            MAX_SHORT_STRING_LEN,
1450            "package_identifier",
1451        )?;
1452        let config_str = self.config.to_string();
1453        check_string_len(&config_str, MAX_PLUGIN_CONFIG_JSON_LEN, "config")?;
1454        Ok(())
1455    }
1456}
1457
1458impl WireValidate for TestPluginConfigResultPayload {
1459    fn wire_validate(&self) -> Result<(), WireValidationError> {
1460        check_string_len(&self.request_id, MAX_SHORT_STRING_LEN, "request_id")?;
1461        check_opt_string_len(&self.output, MAX_CONFIG_TEST_OUTPUT_LEN, "output")?;
1462        check_opt_string_len(&self.error, MAX_MEDIUM_STRING_LEN, "error")?;
1463        check_opt_string_len(
1464            &self.detected_version,
1465            MAX_SHORT_STRING_LEN,
1466            "detected_version",
1467        )?;
1468        Ok(())
1469    }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475
1476    #[test]
1477    fn service_message_report_hosts_validates() {
1478        let msg = ServiceMessage::ReportHosts(ReportHostsPayload {
1479            hosts: vec![HostInfo {
1480                machine_id: "test-id".to_string(),
1481                os_type: Some("linux".to_string()),
1482                os_version: None,
1483                architecture: None,
1484                hostname: None,
1485                ip_address: None,
1486                agent_host_id: None,
1487                features: None,
1488            }],
1489            agent_version: "1.0.0".to_string(),
1490            capabilities: std::collections::BTreeSet::new(),
1491        });
1492        assert!(msg.wire_validate().is_ok());
1493    }
1494
1495    #[test]
1496    fn service_message_report_hosts_too_many() {
1497        let hosts: Vec<HostInfo> = (0..MAX_REPORT_HOSTS + 1)
1498            .map(|i| HostInfo {
1499                machine_id: format!("host-{i}"),
1500                os_type: None,
1501                os_version: None,
1502                architecture: None,
1503                hostname: None,
1504                ip_address: None,
1505                agent_host_id: None,
1506                features: None,
1507            })
1508            .collect();
1509        let msg = ServiceMessage::ReportHosts(ReportHostsPayload {
1510            hosts,
1511            agent_version: "1.0.0".to_string(),
1512            capabilities: std::collections::BTreeSet::new(),
1513        });
1514        let err = msg.wire_validate().unwrap_err();
1515        assert_eq!(err.field, "hosts");
1516    }
1517
1518    #[test]
1519    fn controller_message_check_versions_validates() {
1520        let msg = ControllerMessage::CheckVersions(CheckVersionsPayload {
1521            host_machine_id: "test".to_string(),
1522            assignments: vec![],
1523        });
1524        assert!(msg.wire_validate().is_ok());
1525    }
1526
1527    #[test]
1528    fn controller_message_check_versions_too_many() {
1529        let assignments: Vec<VersionCheckAssignment> = (0..MAX_VERSION_CHECK_ASSIGNMENTS + 1)
1530            .map(|i| VersionCheckAssignment {
1531                software_item_id: uuid::Uuid::nil(),
1532                name: format!("item-{i}"),
1533                detect_version: None,
1534                fetch_releases: None,
1535                host_software_item_id: None,
1536            })
1537            .collect();
1538        let msg = ControllerMessage::CheckVersions(CheckVersionsPayload {
1539            host_machine_id: "test".to_string(),
1540            assignments,
1541        });
1542        let err = msg.wire_validate().unwrap_err();
1543        assert_eq!(err.field, "assignments");
1544    }
1545
1546    #[test]
1547    fn set_update_freeze_validates() {
1548        let payload = SetUpdateFreezePayload {
1549            enabled: true,
1550            reason: Some("test".to_string()),
1551        };
1552        assert!(payload.wire_validate().is_ok());
1553    }
1554
1555    #[test]
1556    fn set_update_freeze_reason_too_long() {
1557        let payload = SetUpdateFreezePayload {
1558            enabled: true,
1559            reason: Some("x".repeat(MAX_MEDIUM_STRING_LEN + 1)),
1560        };
1561        assert!(payload.wire_validate().is_err());
1562    }
1563
1564    #[test]
1565    fn release_asset_validates() {
1566        let asset = ReleaseAsset {
1567            name: "app.tar.gz".to_string(),
1568            download_url: "https://example.com/app".to_string(),
1569            size: None,
1570            content_type: None,
1571            sha256_digest: Some("a".repeat(64)),
1572        };
1573        assert!(asset.wire_validate().is_ok());
1574    }
1575
1576    #[test]
1577    fn release_asset_invalid_digest_wrong_length() {
1578        let asset = ReleaseAsset {
1579            name: "app.tar.gz".to_string(),
1580            download_url: "https://example.com/app".to_string(),
1581            size: None,
1582            content_type: None,
1583            sha256_digest: Some("abc".to_string()),
1584        };
1585        let err = asset.wire_validate().unwrap_err();
1586        assert_eq!(err.field, "asset.sha256_digest");
1587    }
1588
1589    #[test]
1590    fn release_asset_invalid_digest_non_hex() {
1591        let asset = ReleaseAsset {
1592            name: "app.tar.gz".to_string(),
1593            download_url: "https://example.com/app".to_string(),
1594            size: None,
1595            content_type: None,
1596            sha256_digest: Some("z".repeat(64)),
1597        };
1598        let err = asset.wire_validate().unwrap_err();
1599        assert_eq!(err.field, "asset.sha256_digest");
1600    }
1601
1602    #[test]
1603    fn release_info_validates() {
1604        let info = ReleaseInfo {
1605            tag: "v1.0.0".to_string(),
1606            release_url: "https://example.com/release".to_string(),
1607            assets: vec![],
1608            attestation_status: None,
1609            require_attestation: false,
1610        };
1611        assert!(info.wire_validate().is_ok());
1612    }
1613
1614    #[test]
1615    fn release_info_too_many_assets() {
1616        let assets: Vec<ReleaseAsset> = (0..MAX_RELEASE_ASSETS + 1)
1617            .map(|i| ReleaseAsset {
1618                name: format!("asset-{i}"),
1619                download_url: format!("https://example.com/{i}"),
1620                size: None,
1621                content_type: None,
1622                sha256_digest: None,
1623            })
1624            .collect();
1625        let info = ReleaseInfo {
1626            tag: "v1.0.0".to_string(),
1627            release_url: "https://example.com".to_string(),
1628            assets,
1629            attestation_status: None,
1630            require_attestation: false,
1631        };
1632        let err = info.wire_validate().unwrap_err();
1633        assert_eq!(err.field, "release_info.assets");
1634    }
1635
1636    #[test]
1637    fn execute_update_validates() {
1638        let payload = ExecuteUpdatePayload {
1639            host_machine_id: "test".to_string(),
1640            update_history_id: uuid::Uuid::nil(),
1641            software_item_id: uuid::Uuid::nil(),
1642            software_item_name: "test".to_string(),
1643            to_version: "1.0".to_string(),
1644            detect_version_plugin: None,
1645            execute_update_plugin: PluginAssignment {
1646                plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
1647                package_identifier: "test".to_string(),
1648                config: serde_json::json!({}),
1649            },
1650            pre_update_hook_plugins: vec![],
1651            post_update_hook_plugins: vec![],
1652            release_info: Some(ReleaseInfo {
1653                tag: "v1.0".to_string(),
1654                release_url: "https://example.com".to_string(),
1655                assets: vec![],
1656                attestation_status: None,
1657                require_attestation: false,
1658            }),
1659            timeout: std::time::Duration::from_secs(60),
1660            interactive: false,
1661        };
1662        assert!(payload.wire_validate().is_ok());
1663    }
1664
1665    #[test]
1666    fn execute_update_too_many_hook_plugins() {
1667        let plugins: Vec<PluginAssignment> = (0..MAX_UPDATE_HOOKS + 1)
1668            .map(|_| PluginAssignment {
1669                plugin_type: plugin_ids::HOOK_SHELL.clone(),
1670                package_identifier: String::new(),
1671                config: serde_json::json!({}),
1672            })
1673            .collect();
1674        let payload = ExecuteUpdatePayload {
1675            host_machine_id: "test".to_string(),
1676            update_history_id: uuid::Uuid::nil(),
1677            software_item_id: uuid::Uuid::nil(),
1678            software_item_name: "test".to_string(),
1679            to_version: "1.0".to_string(),
1680            detect_version_plugin: None,
1681            execute_update_plugin: PluginAssignment {
1682                plugin_type: plugin_ids::RELEASES_GITHUB.clone(),
1683                package_identifier: "test".to_string(),
1684                config: serde_json::json!({}),
1685            },
1686            pre_update_hook_plugins: plugins,
1687            post_update_hook_plugins: vec![],
1688            release_info: None,
1689            timeout: std::time::Duration::from_secs(60),
1690            interactive: false,
1691        };
1692        let err = payload.wire_validate().unwrap_err();
1693        assert_eq!(err.field, "pre_update_hook_plugins");
1694    }
1695
1696    #[test]
1697    fn discovery_results_validates() {
1698        let payload = DiscoveryResultsPayload {
1699            host_machine_id: "test".to_string(),
1700            results: vec![],
1701        };
1702        assert!(payload.wire_validate().is_ok());
1703    }
1704
1705    #[test]
1706    fn unknown_service_message_passes() {
1707        let msg = ServiceMessage::Unknown;
1708        assert!(msg.wire_validate().is_ok());
1709    }
1710
1711    #[test]
1712    fn unknown_controller_message_passes() {
1713        let msg = ControllerMessage::Unknown;
1714        assert!(msg.wire_validate().is_ok());
1715    }
1716
1717    #[test]
1718    fn batch_update_result_validates() {
1719        let payload = BatchUpdateResultPayload {
1720            batch_id: uuid::Uuid::nil(),
1721            results: vec![],
1722        };
1723        assert!(payload.wire_validate().is_ok());
1724    }
1725
1726    #[test]
1727    fn batch_update_result_too_many() {
1728        let results: Vec<BatchUpdateItemResult> = (0..MAX_BATCH_UPDATE_RESULTS + 1)
1729            .map(|_| BatchUpdateItemResult {
1730                host_software_item_id: uuid::Uuid::nil(),
1731                update_history_id: uuid::Uuid::nil(),
1732                status: UpdateFinalStatus::Completed,
1733                output: String::new(),
1734                installed_version: None,
1735                error: None,
1736            })
1737            .collect();
1738        let payload = BatchUpdateResultPayload {
1739            batch_id: uuid::Uuid::nil(),
1740            results,
1741        };
1742        let err = payload.wire_validate().unwrap_err();
1743        assert_eq!(err.field, "results");
1744    }
1745
1746    // ── Extension wire validation tests ─────────────────────────────────────
1747
1748    fn test_surface_registration() -> surfaces::SurfaceRegistration {
1749        surfaces::SurfaceRegistration {
1750            provider: surfaces::ProviderIdentity {
1751                provider_id: "uptrakit-agent-ssh".to_string(),
1752                provider_kind: surfaces::ProviderKind::Service,
1753                provider_namespace: "uptrakit.agent.ssh".to_string(),
1754            },
1755            framework_generation: surfaces::FrameworkGeneration::new(1, 0),
1756            capabilities: surfaces::CapabilitySet::default(),
1757            effective_tenant_binding: surfaces::EffectiveTenantBinding {
1758                scope: surfaces::Scope::Tenant,
1759                tenant_id: Some(uuid::Uuid::nil().to_string()),
1760            },
1761            surfaces: vec![surfaces::RegisteredSurface {
1762                descriptor: surfaces::SurfaceDescriptor::builder()
1763                    .surface_id(surfaces::SurfaceId::new("ssh.guest.panel").unwrap())
1764                    .label("SSH Guests")
1765                    .priority(100)
1766                    .slot(surfaces::SLOT_SETTINGS_TABS)
1767                    .scope(surfaces::Scope::Tenant)
1768                    .targeting(surfaces::Targeting::Universal)
1769                    .provider_kind(surfaces::ProviderKind::Service)
1770                    .required_capabilities(surfaces::CapabilitySet::default())
1771                    .root_node(surfaces::SurfaceNode::Section {
1772                        title: Some("Guests".to_string()),
1773                        children: vec![surfaces::SurfaceNode::TextBlock {
1774                            text: "Guests view".to_string(),
1775                        }],
1776                    })
1777                    .build(),
1778                interactions: vec![surfaces::InteractionDescriptor {
1779                    interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1780                    kind: surfaces::InteractionKind::MutationAction,
1781                    label: "Refresh".to_string(),
1782                    required_permission: None,
1783                    input_schema: None,
1784                    result_schema: None,
1785                    sensitive_fields: vec![],
1786                    timeout_seconds: None,
1787                    confirmation: None,
1788                    transport: surfaces::InteractionTransport::ProviderProxied,
1789                    workflow_steps: vec![],
1790                    form_ui: None,
1791                }],
1792                data_sources: vec![surfaces::DataSourceDescriptor {
1793                    data_source_id: surfaces::DataSourceId::new("guest.rows").unwrap(),
1794                    kind: surfaces::DataSourceKind::Static {
1795                        data: serde_json::json!({"rows": []}),
1796                    },
1797                    result_schema: surfaces::SchemaContract::Object,
1798                    pagination: None,
1799                    sorting: None,
1800                    filtering: None,
1801                    refresh_policy: surfaces::RefreshPolicy::Manual,
1802                    empty_state: None,
1803                }],
1804            }],
1805            encryption_metadata: None,
1806        }
1807    }
1808
1809    fn nested_json_array(depth: usize) -> serde_json::Value {
1810        let mut value = serde_json::json!(0);
1811        for _ in 0..depth {
1812            value = serde_json::json!([value]);
1813        }
1814        value
1815    }
1816
1817    #[test]
1818    fn surface_registration_rejects_oversized_nested_root_node_text() {
1819        let mut payload = test_surface_registration();
1820        payload.surfaces[0].descriptor.root_node = surfaces::SurfaceNode::Section {
1821            title: None,
1822            children: vec![surfaces::SurfaceNode::Tabs {
1823                tabs: vec![surfaces::SurfaceTab {
1824                    id: surfaces::SurfaceTabId::new("guests").unwrap(),
1825                    label: "Guests".to_string(),
1826                    root: surfaces::SurfaceNode::TextBlock {
1827                        text: "x".repeat(MAX_MEDIUM_STRING_LEN + 1),
1828                    },
1829                }],
1830            }],
1831        };
1832
1833        let err = payload.wire_validate().unwrap_err();
1834        assert_eq!(err.field, "surfaces[].descriptor.root_node.text");
1835    }
1836
1837    #[test]
1838    fn surface_registration_rejects_invalid_interaction_confirmation_text() {
1839        let mut payload = test_surface_registration();
1840        payload.surfaces[0].interactions[0] = surfaces::InteractionDescriptor {
1841            interaction_id: surfaces::InteractionId::new("danger.refresh").unwrap(),
1842            kind: surfaces::InteractionKind::ConfirmableAction,
1843            label: "Danger Refresh".to_string(),
1844            required_permission: None,
1845            input_schema: None,
1846            result_schema: None,
1847            sensitive_fields: vec![],
1848            timeout_seconds: None,
1849            confirmation: Some(surfaces::InteractionConfirmation {
1850                title: "Confirm".to_string(),
1851                message: "x".repeat(MAX_MEDIUM_STRING_LEN + 1),
1852                confirm_label: None,
1853                cancel_label: None,
1854                severity: surfaces::ConfirmationSeverity::Warning,
1855            }),
1856            transport: surfaces::InteractionTransport::ProviderProxied,
1857            workflow_steps: vec![],
1858            form_ui: None,
1859        };
1860
1861        let err = payload.wire_validate().unwrap_err();
1862        assert_eq!(err.field, "surfaces[].interactions[].confirmation.message");
1863    }
1864
1865    #[test]
1866    fn surface_registration_rejects_invalid_data_source_metadata() {
1867        let mut payload = test_surface_registration();
1868        payload.surfaces[0].data_sources[0] = surfaces::DataSourceDescriptor {
1869            data_source_id: surfaces::DataSourceId::new("guest.query").unwrap(),
1870            kind: surfaces::DataSourceKind::ProviderQuery {
1871                operation_id: "x".repeat(MAX_SHORT_STRING_LEN + 1),
1872            },
1873            result_schema: surfaces::SchemaContract::Object,
1874            pagination: Some(surfaces::DataSourcePagination {
1875                default_page_size: 100,
1876                max_page_size: 10,
1877            }),
1878            sorting: None,
1879            filtering: None,
1880            refresh_policy: surfaces::RefreshPolicy::Interval { seconds: 0 },
1881            empty_state: None,
1882        };
1883
1884        let err = payload.wire_validate().unwrap_err();
1885        assert_eq!(
1886            err.field,
1887            "surfaces[].data_sources[].kind.provider_query.operation_id"
1888        );
1889    }
1890
1891    #[test]
1892    fn surface_registration_rejects_overdeep_static_data() {
1893        let mut payload = test_surface_registration();
1894        payload.surfaces[0].data_sources[0] = surfaces::DataSourceDescriptor {
1895            data_source_id: surfaces::DataSourceId::new("guest.deep").unwrap(),
1896            kind: surfaces::DataSourceKind::Static {
1897                data: nested_json_array(MAX_SURFACE_JSON_DEPTH + 1),
1898            },
1899            result_schema: surfaces::SchemaContract::Array,
1900            pagination: None,
1901            sorting: None,
1902            filtering: None,
1903            refresh_policy: surfaces::RefreshPolicy::Manual,
1904            empty_state: None,
1905        };
1906
1907        let err = payload.wire_validate().unwrap_err();
1908        assert_eq!(err.field, "surfaces[].data_sources[].kind.static.data");
1909    }
1910
1911    #[test]
1912    fn surface_action_request_rejects_invalid_tenant_uuid() {
1913        let payload = surfaces::SurfaceActionRequest {
1914            request_id: uuid::Uuid::new_v4(),
1915            tenant_id: "not-a-uuid".to_string(),
1916            surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
1917            interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1918            idempotency_key: "idem-1".to_string(),
1919            target_provider_id: None,
1920            caller_origin: surfaces::CallerOrigin::Provider {
1921                provider_id: "uptrakit-agent-ssh".to_string(),
1922            },
1923            params: serde_json::Map::new(),
1924            encrypted_sensitive_params: None,
1925        };
1926
1927        let err = payload.wire_validate().unwrap_err();
1928        assert_eq!(err.field, "tenant_id");
1929    }
1930
1931    #[test]
1932    fn surface_action_request_rejects_overdeep_params_json() {
1933        let payload = surfaces::SurfaceActionRequest {
1934            request_id: uuid::Uuid::new_v4(),
1935            tenant_id: uuid::Uuid::nil().to_string(),
1936            surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
1937            interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1938            idempotency_key: "idem-1".to_string(),
1939            target_provider_id: None,
1940            caller_origin: surfaces::CallerOrigin::Provider {
1941                provider_id: "uptrakit-agent-ssh".to_string(),
1942            },
1943            params: serde_json::json!({
1944                "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
1945            })
1946            .as_object()
1947            .unwrap()
1948            .clone(),
1949            encrypted_sensitive_params: None,
1950        };
1951
1952        let err = payload.wire_validate().unwrap_err();
1953        assert_eq!(err.field, "params");
1954    }
1955
1956    #[test]
1957    fn surface_action_request_rejects_over_node_count_params_json() {
1958        let payload = surfaces::SurfaceActionRequest {
1959            request_id: uuid::Uuid::new_v4(),
1960            tenant_id: uuid::Uuid::nil().to_string(),
1961            surface_id: surfaces::SurfaceId::new("ssh.guest.panel").unwrap(),
1962            interaction_id: surfaces::InteractionId::new("refresh").unwrap(),
1963            idempotency_key: "idem-1".to_string(),
1964            target_provider_id: None,
1965            caller_origin: surfaces::CallerOrigin::Provider {
1966                provider_id: "uptrakit-agent-ssh".to_string(),
1967            },
1968            params: serde_json::json!({
1969                "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
1970            })
1971            .as_object()
1972            .unwrap()
1973            .clone(),
1974            encrypted_sensitive_params: None,
1975        };
1976
1977        let err = payload.wire_validate().unwrap_err();
1978        assert_eq!(err.field, "params");
1979    }
1980
1981    #[test]
1982    fn surface_action_response_rejects_overdeep_result_json() {
1983        let payload = surfaces::SurfaceActionResponse {
1984            request_id: uuid::Uuid::new_v4(),
1985            success: true,
1986            result: Some(serde_json::json!({
1987                "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
1988            })),
1989            error: None,
1990        };
1991
1992        let err = payload.wire_validate().unwrap_err();
1993        assert_eq!(err.field, "result");
1994    }
1995
1996    #[test]
1997    fn surface_action_response_rejects_over_node_count_result_json() {
1998        let payload = surfaces::SurfaceActionResponse {
1999            request_id: uuid::Uuid::new_v4(),
2000            success: true,
2001            result: Some(serde_json::json!({
2002                "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
2003            })),
2004            error: None,
2005        };
2006
2007        let err = payload.wire_validate().unwrap_err();
2008        assert_eq!(err.field, "result");
2009    }
2010
2011    #[test]
2012    fn surface_action_error_rejects_overdeep_details_json() {
2013        let payload = surfaces::SurfaceActionError {
2014            code: surfaces::SurfaceActionErrorCode::InternalError,
2015            message: "bad".to_string(),
2016            details: Some(serde_json::json!({
2017                "payload": nested_json_array(MAX_SURFACE_JSON_DEPTH + 1)
2018            })),
2019        };
2020
2021        let err = payload.wire_validate().unwrap_err();
2022        assert_eq!(err.field, "error.details");
2023    }
2024
2025    #[test]
2026    fn surface_action_error_rejects_over_node_count_details_json() {
2027        let payload = surfaces::SurfaceActionError {
2028            code: surfaces::SurfaceActionErrorCode::InternalError,
2029            message: "bad".to_string(),
2030            details: Some(serde_json::json!({
2031                "payload": vec![0u8; MAX_SURFACE_JSON_NODES + 1]
2032            })),
2033        };
2034
2035        let err = payload.wire_validate().unwrap_err();
2036        assert_eq!(err.field, "error.details");
2037    }
2038
2039    #[test]
2040    fn report_plugin_config_validates() {
2041        let msg = ServiceMessage::ReportPluginConfig(ReportPluginConfigPayload {
2042            request_id: "req-1".to_string(),
2043            plugin_type: "infrastructure_proxmox".to_string(),
2044            name: "pve.local".to_string(),
2045            config: serde_json::json!({"api_url": "https://pve:8006"}),
2046        });
2047        assert!(msg.wire_validate().is_ok());
2048    }
2049
2050    #[test]
2051    fn report_plugin_config_response_validates() {
2052        let msg =
2053            ControllerMessage::ReportPluginConfigResponse(ReportPluginConfigResponsePayload {
2054                request_id: "req-1".to_string(),
2055                success: true,
2056                plugin_config_id: Some(uuid::Uuid::nil()),
2057                error: None,
2058            });
2059        assert!(msg.wire_validate().is_ok());
2060    }
2061
2062    #[test]
2063    fn report_plugin_config_rejects_oversized_config() {
2064        let msg = ServiceMessage::ReportPluginConfig(ReportPluginConfigPayload {
2065            request_id: "req-1".to_string(),
2066            plugin_type: "infrastructure_proxmox".to_string(),
2067            name: "pve.local".to_string(),
2068            config: serde_json::Value::String("x".repeat(MAX_PLUGIN_CONFIG_JSON_LEN + 1)),
2069        });
2070        assert!(msg.wire_validate().is_err());
2071    }
2072
2073    #[test]
2074    fn update_stdin_data_validates() {
2075        let msg = ControllerMessage::UpdateStdinData(UpdateStdinDataPayload {
2076            update_history_id: uuid::Uuid::nil(),
2077            data: "aGVsbG8=".to_string(),
2078            signal: None,
2079        });
2080        assert!(msg.wire_validate().is_ok());
2081    }
2082
2083    #[test]
2084    fn update_stdin_data_rejects_oversized_data() {
2085        let msg = ControllerMessage::UpdateStdinData(UpdateStdinDataPayload {
2086            update_history_id: uuid::Uuid::nil(),
2087            data: "x".repeat(MAX_STDIN_DATA_LEN + 1),
2088            signal: None,
2089        });
2090        assert!(msg.wire_validate().is_err());
2091    }
2092
2093    #[test]
2094    fn stdin_attention_validates() {
2095        let msg = ServiceMessage::StdinAttention(StdinAttentionPayload {
2096            update_history_id: uuid::Uuid::nil(),
2097            hint: Some("waiting for config file conflict resolution".to_string()),
2098        });
2099        assert!(msg.wire_validate().is_ok());
2100    }
2101
2102    #[test]
2103    fn stdin_attention_rejects_oversized_hint() {
2104        let msg = ServiceMessage::StdinAttention(StdinAttentionPayload {
2105            update_history_id: uuid::Uuid::nil(),
2106            hint: Some("x".repeat(MAX_MEDIUM_STRING_LEN + 1)),
2107        });
2108        assert!(msg.wire_validate().is_err());
2109    }
2110
2111    #[test]
2112    fn service_settings_report_page_limits_validate() {
2113        let msg = ControllerMessage::ServiceSettings(ServiceSettingsPayload {
2114            renewal_window_hours: 6,
2115            ca_bundle_hash: "hash".to_string(),
2116            capabilities: std::collections::BTreeSet::new(),
2117            report_page_limits: ReportPageLimits::default(),
2118            shutdown_timeout: None,
2119            ping_interval: std::time::Duration::from_secs(30),
2120            tenant_id: None,
2121        });
2122
2123        assert!(msg.wire_validate().is_ok());
2124    }
2125
2126    #[test]
2127    fn service_settings_reject_zero_report_page_limit() {
2128        let msg = ControllerMessage::ServiceSettings(ServiceSettingsPayload {
2129            renewal_window_hours: 6,
2130            ca_bundle_hash: "hash".to_string(),
2131            capabilities: std::collections::BTreeSet::new(),
2132            report_page_limits: ReportPageLimits {
2133                report_hosts: 0,
2134                ..ReportPageLimits::default()
2135            },
2136            shutdown_timeout: None,
2137            ping_interval: std::time::Duration::from_secs(30),
2138            tenant_id: None,
2139        });
2140
2141        let err = msg.wire_validate().unwrap_err();
2142        assert_eq!(err.field, "report_page_limits.report_hosts");
2143    }
2144}