Skip to main content

canic_host/deployment_truth/
report.rs

1use super::*;
2use std::collections::{BTreeMap, BTreeSet};
3
4///
5/// LocalDeploymentCheckRequest
6///
7#[derive(Clone, Debug, Eq, PartialEq)]
8pub struct LocalDeploymentCheckRequest {
9    pub deployment_name: String,
10    pub network: String,
11    pub workspace_root: std::path::PathBuf,
12    pub icp_root: std::path::PathBuf,
13    pub config_path: Option<std::path::PathBuf>,
14    pub observed_at: String,
15    pub runtime_variant: String,
16    pub build_profile: String,
17}
18
19/// Build local plan and inventory, then return the passive safety check bundle.
20pub fn check_local_deployment(
21    request: &LocalDeploymentCheckRequest,
22) -> Result<DeploymentCheckV1, DeploymentTruthError> {
23    let plan = build_local_deployment_plan(&LocalDeploymentPlanRequest {
24        deployment_name: request.deployment_name.clone(),
25        network: request.network.clone(),
26        workspace_root: request.workspace_root.clone(),
27        icp_root: request.icp_root.clone(),
28        config_path: request.config_path.clone(),
29        runtime_variant: request.runtime_variant.clone(),
30        build_profile: request.build_profile.clone(),
31    });
32    let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
33        deployment_name: request.deployment_name.clone(),
34        network: request.network.clone(),
35        workspace_root: request.workspace_root.clone(),
36        icp_root: request.icp_root.clone(),
37        config_path: request.config_path.clone(),
38        observed_at: request.observed_at.clone(),
39    })?;
40    let diff = compare_plan_to_inventory(&plan, &inventory);
41    let report = safety_report_from_diff(
42        format!(
43            "local:{}:{}:report",
44            request.network, request.deployment_name
45        ),
46        Some(format!(
47            "local:{}:{}:diff",
48            request.network, request.deployment_name
49        )),
50        &diff,
51    );
52
53    Ok(DeploymentCheckV1 {
54        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
55        check_id: format!(
56            "local:{}:{}:check",
57            request.network, request.deployment_name
58        ),
59        plan,
60        inventory,
61        diff,
62        report,
63    })
64}
65
66/// Compare intended deployment state with observed inventory into a machine diff.
67#[must_use]
68pub fn compare_plan_to_inventory(
69    plan: &DeploymentPlanV1,
70    inventory: &DeploymentInventoryV1,
71) -> DeploymentDiffV1 {
72    let mut artifact_diff = Vec::new();
73    let mut controller_diff = Vec::new();
74    let pool_diff = Vec::new();
75    let mut embedded_config_diff = Vec::new();
76    let mut module_hash_diff = Vec::new();
77    let mut verifier_readiness_diff = Vec::new();
78    let mut hard_failures = Vec::new();
79    let mut warnings = Vec::new();
80
81    compare_identity(plan, inventory, &mut hard_failures);
82    compare_authority_profile(plan, &mut controller_diff, &mut hard_failures);
83    compare_artifacts(
84        plan,
85        inventory,
86        &mut artifact_diff,
87        &mut hard_failures,
88        &mut warnings,
89    );
90    compare_canisters(
91        plan,
92        inventory,
93        &mut controller_diff,
94        &mut hard_failures,
95        &mut warnings,
96    );
97    compare_module_hashes(
98        plan,
99        inventory,
100        &mut module_hash_diff,
101        &mut hard_failures,
102        &mut warnings,
103    );
104    compare_raw_config(
105        plan,
106        inventory,
107        &mut embedded_config_diff,
108        &mut hard_failures,
109    );
110    compare_embedded_config(
111        plan,
112        inventory,
113        &mut embedded_config_diff,
114        &mut hard_failures,
115        &mut warnings,
116    );
117    compare_verifier_readiness(plan, inventory, &mut verifier_readiness_diff, &mut warnings);
118    for gap in &inventory.unresolved_observations {
119        warnings.push(SafetyFindingV1 {
120            code: "observation_gap".to_string(),
121            message: gap.description.clone(),
122            severity: SafetySeverityV1::Warning,
123            subject: Some(gap.key.clone()),
124        });
125    }
126
127    let status = safety_status(&hard_failures, &warnings);
128    DeploymentDiffV1 {
129        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
130        plan_identity: plan.deployment_identity.clone(),
131        observed_identity: inventory.observed_identity.clone(),
132        artifact_diff,
133        controller_diff,
134        pool_diff,
135        embedded_config_diff,
136        module_hash_diff,
137        verifier_readiness_diff,
138        resume_safety: ResumeSafetyV1 {
139            status,
140            reasons: resume_safety_reasons(&hard_failures, &warnings),
141        },
142        hard_failures,
143        warnings,
144        resumable_phases: Vec::new(),
145    }
146}
147
148/// Render an operator-facing safety report from a machine deployment diff.
149#[must_use]
150pub fn safety_report_from_diff(
151    report_id: impl Into<String>,
152    diff_id: Option<String>,
153    diff: &DeploymentDiffV1,
154) -> SafetyReportV1 {
155    let status = safety_status(&diff.hard_failures, &diff.warnings);
156    SafetyReportV1 {
157        schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
158        report_id: report_id.into(),
159        diff_id,
160        status,
161        summary: safety_summary(status, diff.hard_failures.len(), diff.warnings.len()),
162        hard_failures: diff.hard_failures.clone(),
163        warnings: diff.warnings.clone(),
164        next_actions: safety_next_actions(status),
165    }
166}
167
168fn compare_identity(
169    plan: &DeploymentPlanV1,
170    inventory: &DeploymentInventoryV1,
171    hard_failures: &mut Vec<SafetyFindingV1>,
172) {
173    let Some(observed) = &inventory.observed_identity else {
174        hard_failures.push(finding(
175            "identity_unobserved",
176            "deployment identity was not observed",
177            SafetySeverityV1::HardFailure,
178            None,
179        ));
180        return;
181    };
182
183    if observed.network != plan.deployment_identity.network {
184        hard_failures.push(finding(
185            "network_mismatch",
186            format!(
187                "plan network {} differs from observed network {}",
188                plan.deployment_identity.network, observed.network
189            ),
190            SafetySeverityV1::HardFailure,
191            Some("deployment_identity.network".to_string()),
192        ));
193    }
194    if let (Some(expected), Some(actual)) = (
195        plan.deployment_identity.root_principal.as_ref(),
196        observed.root_principal.as_ref(),
197    ) && expected != actual
198    {
199        hard_failures.push(finding(
200            "root_trust_anchor_mismatch",
201            format!("plan root {expected} differs from observed root {actual}"),
202            SafetySeverityV1::HardFailure,
203            Some("deployment_identity.root_principal".to_string()),
204        ));
205    }
206    match (
207        plan.deployment_identity.deployment_manifest_digest.as_ref(),
208        observed.deployment_manifest_digest.as_ref(),
209    ) {
210        (Some(expected), Some(actual)) if expected != actual => {
211            hard_failures.push(finding(
212                "deployment_manifest_mismatch",
213                "deployment manifest digest differs from the observed local config",
214                SafetySeverityV1::HardFailure,
215                Some("deployment_identity.deployment_manifest_digest".to_string()),
216            ));
217        }
218        (Some(_), None) => {
219            hard_failures.push(finding(
220                "deployment_manifest_unobserved",
221                "deployment manifest digest was not observed",
222                SafetySeverityV1::HardFailure,
223                Some("deployment_identity.deployment_manifest_digest".to_string()),
224            ));
225        }
226        _ => {}
227    }
228}
229
230fn compare_authority_profile(
231    plan: &DeploymentPlanV1,
232    controller_diff: &mut Vec<DiffItemV1>,
233    hard_failures: &mut Vec<SafetyFindingV1>,
234) {
235    let mut reported = BTreeSet::new();
236    for controller in &plan.authority_profile.expected_controllers {
237        if !is_staging_or_emergency_controller(plan, controller) {
238            continue;
239        }
240        if !reported.insert(controller.as_str()) {
241            continue;
242        }
243        controller_diff.push(diff_item(
244            "controller_authority_overlap",
245            "authority_profile",
246            Some("expected-only".to_string()),
247            Some(controller.clone()),
248            SafetySeverityV1::HardFailure,
249        ));
250        hard_failures.push(finding(
251            "controller_authority_overlap",
252            format!(
253                "controller {controller} appears in both expected and staging/emergency authority"
254            ),
255            SafetySeverityV1::HardFailure,
256            Some("authority_profile".to_string()),
257        ));
258    }
259}
260
261fn compare_artifacts(
262    plan: &DeploymentPlanV1,
263    inventory: &DeploymentInventoryV1,
264    artifact_diff: &mut Vec<DiffItemV1>,
265    hard_failures: &mut Vec<SafetyFindingV1>,
266    warnings: &mut Vec<SafetyFindingV1>,
267) {
268    let observed_by_role = inventory
269        .observed_artifacts
270        .iter()
271        .map(|artifact| (artifact.role.as_str(), artifact))
272        .collect::<BTreeMap<_, _>>();
273
274    for expected in &plan.role_artifacts {
275        let Some(observed) = observed_by_role.get(expected.role.as_str()) else {
276            artifact_diff.push(diff_item(
277                "artifact",
278                &expected.role,
279                expected.wasm_gz_path.clone(),
280                None,
281                SafetySeverityV1::HardFailure,
282            ));
283            hard_failures.push(finding(
284                "artifact_missing",
285                format!("missing observed artifact for role {}", expected.role),
286                SafetySeverityV1::HardFailure,
287                Some(expected.role.clone()),
288            ));
289            continue;
290        };
291
292        compare_artifact_file_sha256(expected, observed, artifact_diff, hard_failures);
293
294        match (
295            expected.wasm_gz_sha256.as_ref(),
296            observed.payload_sha256.as_ref(),
297        ) {
298            (Some(want), Some(got)) if want != got => {
299                artifact_diff.push(diff_item(
300                    "artifact_sha256",
301                    &expected.role,
302                    Some(want.clone()),
303                    Some(got.clone()),
304                    SafetySeverityV1::HardFailure,
305                ));
306                hard_failures.push(finding(
307                    "artifact_digest_mismatch",
308                    format!("artifact digest mismatch for role {}", expected.role),
309                    SafetySeverityV1::HardFailure,
310                    Some(expected.role.clone()),
311                ));
312            }
313            (Some(want), None) => warnings.push(finding(
314                "artifact_digest_unobserved",
315                format!(
316                    "expected artifact digest {want} for role {} was not observed",
317                    expected.role
318                ),
319                SafetySeverityV1::Warning,
320                Some(expected.role.clone()),
321            )),
322            _ => {}
323        }
324    }
325}
326
327fn compare_artifact_file_sha256(
328    expected: &RoleArtifactV1,
329    observed: &ObservedArtifactV1,
330    artifact_diff: &mut Vec<DiffItemV1>,
331    hard_failures: &mut Vec<SafetyFindingV1>,
332) {
333    match (
334        expected.observed_wasm_gz_file_sha256.as_ref(),
335        observed.file_sha256.as_ref(),
336    ) {
337        (Some(want), Some(got)) if want != got => {
338            artifact_diff.push(diff_item(
339                "artifact_file_sha256",
340                &expected.role,
341                Some(want.clone()),
342                Some(got.clone()),
343                SafetySeverityV1::HardFailure,
344            ));
345            hard_failures.push(finding(
346                "artifact_file_digest_mismatch",
347                format!(
348                    "observed artifact file digest changed during deployment truth check for role {}",
349                    expected.role
350                ),
351                SafetySeverityV1::HardFailure,
352                Some(expected.role.clone()),
353            ));
354        }
355        (_, Some(got)) => {
356            artifact_diff.push(diff_item(
357                "artifact_file_sha256",
358                &expected.role,
359                expected.observed_wasm_gz_file_sha256.clone(),
360                Some(got.clone()),
361                SafetySeverityV1::Info,
362            ));
363        }
364        _ => {}
365    }
366}
367
368fn compare_canisters(
369    plan: &DeploymentPlanV1,
370    inventory: &DeploymentInventoryV1,
371    controller_diff: &mut Vec<DiffItemV1>,
372    hard_failures: &mut Vec<SafetyFindingV1>,
373    warnings: &mut Vec<SafetyFindingV1>,
374) {
375    for expected in &plan.expected_canisters {
376        let observed = expected.canister_id.as_ref().map_or_else(
377            || {
378                inventory
379                    .observed_canisters
380                    .iter()
381                    .find(|canister| canister.role.as_deref() == Some(expected.role.as_str()))
382            },
383            |id| {
384                inventory
385                    .observed_canisters
386                    .iter()
387                    .find(|canister| &canister.canister_id == id)
388            },
389        );
390        let Some(observed) = observed else {
391            let severity = if expected.canister_id.is_some() {
392                SafetySeverityV1::HardFailure
393            } else {
394                SafetySeverityV1::Warning
395            };
396            controller_diff.push(diff_item(
397                "canister",
398                &expected.role,
399                expected.canister_id.clone(),
400                None,
401                severity,
402            ));
403            let finding = finding(
404                if expected.canister_id.is_some() {
405                    "canister_missing"
406                } else {
407                    "canister_unobserved"
408                },
409                format!("missing observed canister for role {}", expected.role),
410                severity,
411                Some(expected.role.clone()),
412            );
413            if expected.canister_id.is_some() {
414                hard_failures.push(finding);
415            } else {
416                warnings.push(finding);
417            }
418            continue;
419        };
420        if matches!(
421            observed.control_class,
422            CanisterControlClassV1::UnknownUnsafe | CanisterControlClassV1::UserControlled
423        ) && expected.control_class == CanisterControlClassV1::DeploymentControlled
424        {
425            controller_diff.push(diff_item(
426                "control_class",
427                &expected.role,
428                Some("DeploymentControlled".to_string()),
429                Some(format!("{:?}", observed.control_class)),
430                SafetySeverityV1::HardFailure,
431            ));
432            hard_failures.push(finding(
433                "unsafe_control_class",
434                format!("role {} has unsafe observed control class", expected.role),
435                SafetySeverityV1::HardFailure,
436                Some(expected.role.clone()),
437            ));
438        }
439        compare_role_controllers(plan, observed, controller_diff, hard_failures, warnings);
440    }
441}
442
443fn compare_role_controllers(
444    plan: &DeploymentPlanV1,
445    observed: &ObservedCanisterV1,
446    controller_diff: &mut Vec<DiffItemV1>,
447    hard_failures: &mut Vec<SafetyFindingV1>,
448    warnings: &mut Vec<SafetyFindingV1>,
449) {
450    let role = observed.role.as_deref().unwrap_or("unknown");
451    for expected in &plan.authority_profile.expected_controllers {
452        if observed
453            .controllers
454            .iter()
455            .any(|controller| controller == expected)
456        {
457            continue;
458        }
459        controller_diff.push(diff_item(
460            "controller_missing",
461            role,
462            Some(expected.clone()),
463            Some(controller_set_label(&observed.controllers)),
464            SafetySeverityV1::HardFailure,
465        ));
466        hard_failures.push(finding(
467            "expected_controller_missing",
468            format!("role {role} is missing expected controller {expected}"),
469            SafetySeverityV1::HardFailure,
470            Some(role.to_string()),
471        ));
472    }
473
474    for observed_controller in &observed.controllers {
475        if is_declared_controller(plan, observed_controller) {
476            continue;
477        }
478        controller_diff.push(diff_item(
479            "controller_extra",
480            role,
481            Some(controller_set_label(
482                &plan.authority_profile.expected_controllers,
483            )),
484            Some(observed_controller.clone()),
485            SafetySeverityV1::Warning,
486        ));
487        warnings.push(finding(
488            "extra_controller_observed",
489            format!("role {role} has controller outside the expected authority profile"),
490            SafetySeverityV1::Warning,
491            Some(role.to_string()),
492        ));
493    }
494}
495
496fn is_declared_controller(plan: &DeploymentPlanV1, controller: &str) -> bool {
497    plan.authority_profile
498        .expected_controllers
499        .iter()
500        .chain(plan.authority_profile.staging_controllers.iter())
501        .chain(plan.authority_profile.emergency_controllers.iter())
502        .any(|expected| expected == controller)
503}
504
505fn is_staging_or_emergency_controller(plan: &DeploymentPlanV1, controller: &str) -> bool {
506    plan.authority_profile
507        .staging_controllers
508        .iter()
509        .chain(plan.authority_profile.emergency_controllers.iter())
510        .any(|declared| declared == controller)
511}
512
513fn controller_set_label(controllers: &[String]) -> String {
514    if controllers.is_empty() {
515        return "<none>".to_string();
516    }
517    controllers.join(",")
518}
519
520fn compare_module_hashes(
521    plan: &DeploymentPlanV1,
522    inventory: &DeploymentInventoryV1,
523    module_hash_diff: &mut Vec<DiffItemV1>,
524    hard_failures: &mut Vec<SafetyFindingV1>,
525    warnings: &mut Vec<SafetyFindingV1>,
526) {
527    let observed_by_role = inventory
528        .observed_canisters
529        .iter()
530        .filter_map(|canister| canister.role.as_deref().map(|role| (role, canister)))
531        .collect::<BTreeMap<_, _>>();
532
533    for artifact in &plan.role_artifacts {
534        let Some(expected) = artifact.installed_module_hash.as_ref() else {
535            continue;
536        };
537        let Some(observed_canister) = observed_by_role.get(artifact.role.as_str()) else {
538            continue;
539        };
540        match observed_canister.module_hash.as_ref() {
541            Some(observed) if observed != expected => {
542                module_hash_diff.push(diff_item(
543                    "installed_module_hash",
544                    &artifact.role,
545                    Some(expected.clone()),
546                    Some(observed.clone()),
547                    SafetySeverityV1::HardFailure,
548                ));
549                hard_failures.push(finding(
550                    "installed_module_hash_mismatch",
551                    format!("installed module hash differs for role {}", artifact.role),
552                    SafetySeverityV1::HardFailure,
553                    Some(artifact.role.clone()),
554                ));
555            }
556            None => warnings.push(finding(
557                "installed_module_hash_unobserved",
558                format!(
559                    "installed module hash was not observed for role {}",
560                    artifact.role
561                ),
562                SafetySeverityV1::Warning,
563                Some(artifact.role.clone()),
564            )),
565            _ => {}
566        }
567    }
568}
569
570fn compare_raw_config(
571    plan: &DeploymentPlanV1,
572    inventory: &DeploymentInventoryV1,
573    embedded_config_diff: &mut Vec<DiffItemV1>,
574    hard_failures: &mut Vec<SafetyFindingV1>,
575) {
576    let mut expected = plan
577        .role_artifacts
578        .iter()
579        .filter_map(|artifact| artifact.raw_config_sha256.as_ref())
580        .collect::<Vec<_>>();
581    expected.sort_unstable();
582    expected.dedup();
583    let [expected] = expected.as_slice() else {
584        if expected.len() > 1 {
585            hard_failures.push(finding(
586                "raw_config_plan_inconsistent",
587                "planned role artifacts disagree on raw config digest",
588                SafetySeverityV1::HardFailure,
589                Some("role_artifacts.raw_config_sha256".to_string()),
590            ));
591        }
592        return;
593    };
594
595    if let Some(observed) = &inventory.local_config.raw_config_sha256
596        && observed != *expected
597    {
598        embedded_config_diff.push(diff_item(
599            "raw_config_sha256",
600            "deployment",
601            Some((*expected).clone()),
602            Some(observed.clone()),
603            SafetySeverityV1::HardFailure,
604        ));
605        hard_failures.push(finding(
606            "raw_config_digest_mismatch",
607            "raw local config digest changed during deployment truth check",
608            SafetySeverityV1::HardFailure,
609            Some("local_config.raw_sha256".to_string()),
610        ));
611    }
612}
613
614fn compare_embedded_config(
615    plan: &DeploymentPlanV1,
616    inventory: &DeploymentInventoryV1,
617    embedded_config_diff: &mut Vec<DiffItemV1>,
618    hard_failures: &mut Vec<SafetyFindingV1>,
619    warnings: &mut Vec<SafetyFindingV1>,
620) {
621    let Some(expected) = &plan.deployment_identity.canonical_runtime_config_digest else {
622        return;
623    };
624    match &inventory.local_config.canonical_embedded_config_sha256 {
625        Some(observed) if observed != expected => {
626            embedded_config_diff.push(diff_item(
627                "canonical_config",
628                "deployment",
629                Some(expected.clone()),
630                Some(observed.clone()),
631                SafetySeverityV1::HardFailure,
632            ));
633            hard_failures.push(finding(
634                "canonical_config_mismatch",
635                "canonical runtime config digest differs from the plan",
636                SafetySeverityV1::HardFailure,
637                Some("local_config".to_string()),
638            ));
639        }
640        None => warnings.push(finding(
641            "canonical_config_unobserved",
642            "canonical runtime config digest was not observed",
643            SafetySeverityV1::Warning,
644            Some("local_config".to_string()),
645        )),
646        _ => {}
647    }
648}
649
650fn compare_verifier_readiness(
651    plan: &DeploymentPlanV1,
652    inventory: &DeploymentInventoryV1,
653    verifier_readiness_diff: &mut Vec<DiffItemV1>,
654    warnings: &mut Vec<SafetyFindingV1>,
655) {
656    if !plan.expected_verifier_readiness.required {
657        return;
658    }
659    if inventory.observed_verifier_readiness.status == ObservationStatusV1::NotObserved {
660        verifier_readiness_diff.push(diff_item(
661            "verifier_readiness",
662            "deployment",
663            Some("required".to_string()),
664            Some("not_observed".to_string()),
665            SafetySeverityV1::Warning,
666        ));
667        warnings.push(finding(
668            "verifier_readiness_unobserved",
669            "verifier readiness was required but not observed",
670            SafetySeverityV1::Warning,
671            Some("verifier_readiness".to_string()),
672        ));
673    }
674}
675
676fn finding(
677    code: impl Into<String>,
678    message: impl Into<String>,
679    severity: SafetySeverityV1,
680    subject: Option<String>,
681) -> SafetyFindingV1 {
682    SafetyFindingV1 {
683        code: code.into(),
684        message: message.into(),
685        severity,
686        subject,
687    }
688}
689
690fn diff_item(
691    category: impl Into<String>,
692    subject: impl Into<String>,
693    expected: Option<String>,
694    observed: Option<String>,
695    severity: SafetySeverityV1,
696) -> DiffItemV1 {
697    DiffItemV1 {
698        category: category.into(),
699        subject: subject.into(),
700        expected,
701        observed,
702        severity,
703    }
704}
705
706const fn safety_status(
707    hard_failures: &[SafetyFindingV1],
708    warnings: &[SafetyFindingV1],
709) -> SafetyStatusV1 {
710    if !hard_failures.is_empty() {
711        SafetyStatusV1::Blocked
712    } else if !warnings.is_empty() {
713        SafetyStatusV1::Warning
714    } else {
715        SafetyStatusV1::Safe
716    }
717}
718
719fn resume_safety_reasons(
720    hard_failures: &[SafetyFindingV1],
721    warnings: &[SafetyFindingV1],
722) -> Vec<String> {
723    if !hard_failures.is_empty() {
724        return hard_failures
725            .iter()
726            .map(|finding| finding.message.clone())
727            .collect();
728    }
729    if !warnings.is_empty() {
730        return warnings
731            .iter()
732            .map(|finding| finding.message.clone())
733            .collect();
734    }
735    vec!["no blocking deployment truth differences were found".to_string()]
736}
737
738fn safety_summary(
739    status: SafetyStatusV1,
740    hard_failure_count: usize,
741    warning_count: usize,
742) -> String {
743    match status {
744        SafetyStatusV1::Safe => "deployment inventory matches the checked plan".to_string(),
745        SafetyStatusV1::Warning => {
746            format!("deployment inventory has {warning_count} warning(s)")
747        }
748        SafetyStatusV1::Blocked => {
749            format!(
750                "deployment inventory has {hard_failure_count} blocking issue(s) and {warning_count} warning(s)"
751            )
752        }
753        SafetyStatusV1::NotEvaluated => "deployment safety has not been evaluated".to_string(),
754    }
755}
756
757fn safety_next_actions(status: SafetyStatusV1) -> Vec<String> {
758    match status {
759        SafetyStatusV1::Safe => Vec::new(),
760        SafetyStatusV1::Warning => {
761            vec!["review deployment warnings before continuing".to_string()]
762        }
763        SafetyStatusV1::Blocked => {
764            vec!["resolve blocking deployment truth differences before mutation".to_string()]
765        }
766        SafetyStatusV1::NotEvaluated => vec!["collect deployment inventory".to_string()],
767    }
768}