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