Skip to main content

canwu_sim/runtime/
maintenance.rs

1use super::{
2    CanwuError, CauseRef, DomainRecordChange, DomainRecordMutation, DomainRecordRef, EntityRef,
3    ErrorCode, IngressClass, IngressPayload, IngressReceipt, PersistentDomainRecordStore,
4    PluginRegistry, SimTime, Simulation, StateVisibility, canonical_hash, canonical_text,
5    is_canonical_hash, records, runtime_entity_exists,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::panic::{AssertUnwindSafe, catch_unwind};
10
11pub const OWNER_AUTHORIZED_MAINTENANCE_FORMAT_VERSION: u32 = 1;
12pub const MAX_OWNER_AUTHORIZED_PARTICIPANTS: usize = 32;
13pub const MAX_OWNER_AUTHORIZED_MUTATIONS: usize = 256;
14pub(super) const OWNER_AUTHORIZED_MAINTENANCE_SYSTEM: &str = "owner-authorized-maintenance";
15
16#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum OwnerAuthorizedParticipantRole {
19    TargetOwner,
20    DependentOwner,
21}
22
23#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct OwnerAuthorizedRecordExpectation {
26    pub record: DomainRecordRef,
27    pub version: u64,
28}
29
30#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
31#[serde(deny_unknown_fields)]
32pub struct OwnerAuthorizedMutation {
33    pub mutation: DomainRecordMutation,
34    pub visibility: StateVisibility,
35    pub summary: String,
36}
37
38#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
39#[serde(deny_unknown_fields)]
40pub struct OwnerAuthorizedParticipantDraft {
41    pub plugin: String,
42    pub role: OwnerAuthorizedParticipantRole,
43    pub accepted: bool,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub rejection_reason: Option<String>,
46    pub expected_records: Vec<OwnerAuthorizedRecordExpectation>,
47    pub mutations: Vec<OwnerAuthorizedMutation>,
48}
49
50#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
51#[serde(deny_unknown_fields)]
52pub struct OwnerAuthorizedMaintenanceRequest {
53    pub request_id: String,
54    pub target: OwnerAuthorizedRecordExpectation,
55    pub requested_at: SimTime,
56    #[serde(default)]
57    pub payload: Value,
58}
59
60#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
61#[serde(deny_unknown_fields)]
62pub struct OwnerAuthorizedMaintenanceDraft {
63    pub request_id: String,
64    pub target: OwnerAuthorizedRecordExpectation,
65    pub requested_at: SimTime,
66    pub participants: Vec<OwnerAuthorizedParticipantDraft>,
67}
68
69#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
70#[serde(deny_unknown_fields)]
71pub struct OwnerAuthorizedParticipantProposal {
72    pub plugin: String,
73    pub semantic_hash: String,
74    pub role: OwnerAuthorizedParticipantRole,
75    pub accepted: bool,
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub rejection_reason: Option<String>,
78    pub expected_records: Vec<OwnerAuthorizedRecordExpectation>,
79    pub mutations: Vec<OwnerAuthorizedMutation>,
80    pub proposal_commitment: String,
81}
82
83#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
84#[serde(deny_unknown_fields)]
85pub struct VerifiedOwnerAuthorizedMaintenanceCommit {
86    format_version: u32,
87    request_id: String,
88    target: OwnerAuthorizedRecordExpectation,
89    requested_at: SimTime,
90    source_domain_root: String,
91    participants: Vec<OwnerAuthorizedParticipantProposal>,
92    token: String,
93}
94
95impl VerifiedOwnerAuthorizedMaintenanceCommit {
96    #[must_use]
97    pub(super) fn token(&self) -> &str {
98        &self.token
99    }
100
101    pub(super) fn source_root(&self) -> &str {
102        &self.source_domain_root
103    }
104}
105
106impl Simulation {
107    /// Asks every mandatory owner callback to author its own proposal and
108    /// queues the resulting opaque commit as one canonical maintenance item.
109    pub fn schedule_owner_authorized_maintenance(
110        &mut self,
111        due_at: SimTime,
112        priority: i32,
113        request: OwnerAuthorizedMaintenanceRequest,
114    ) -> Result<IngressReceipt, CanwuError> {
115        let commit = self.prepare_owner_authorized_maintenance(request)?;
116        self.enqueue_owner_authorized_maintenance(due_at, priority, commit)
117    }
118
119    /// Freezes and verifies a bounded set of owner proposals. The kernel fills
120    /// semantic hashes and commitments from the active descriptor registry;
121    /// callers cannot omit a registered dependent owner or mutate a foreign
122    /// schema.
123    fn prepare_owner_authorized_maintenance(
124        &self,
125        request: OwnerAuthorizedMaintenanceRequest,
126    ) -> Result<VerifiedOwnerAuthorizedMaintenanceCommit, CanwuError> {
127        self.ensure_runtime_ready()?;
128        if !canonical_text(&request.request_id) || request.requested_at != self.time() {
129            return Err(invalid_maintenance(
130                "owner-authorized maintenance identity or request time is invalid",
131            ));
132        }
133        let target_record = self
134            .state
135            .current
136            .domain_records
137            .get(&request.target.record)
138            .ok_or_else(|| invalid_maintenance("maintenance target record is unavailable"))?;
139        if target_record.version != request.target.version || !target_record.is_active() {
140            return Err(invalid_maintenance(
141                "maintenance target expectation is stale or not active",
142            ));
143        }
144        let (target_owner, _) = self
145            .plugins
146            .record_schemas
147            .get(&request.target.record.kind)
148            .ok_or_else(|| invalid_maintenance("maintenance target schema has no owner"))?;
149        let mut required = self
150            .plugins
151            .maintenance_dependency_resolvers
152            .get(&request.target.record.kind.namespace)
153            .cloned()
154            .unwrap_or_default();
155        required.insert(target_owner.clone());
156        if required.is_empty() || required.len() > MAX_OWNER_AUTHORIZED_PARTICIPANTS {
157            return Err(invalid_maintenance(
158                "owner-authorized maintenance participant budget is invalid",
159            ));
160        }
161
162        let mut participant_drafts = Vec::with_capacity(required.len());
163        for plugin in &required {
164            let descriptor = self.plugins.descriptors.get(plugin).ok_or_else(|| {
165                invalid_maintenance("maintenance participant has no plugin descriptor")
166            })?;
167            if !descriptor.owner_authorized_maintenance_participant {
168                return Err(invalid_maintenance(
169                    "maintenance participant descriptor lacks an owner callback",
170                ));
171            }
172            let handler = self
173                .plugins
174                .maintenance_participants
175                .get(plugin)
176                .copied()
177                .ok_or_else(|| {
178                    invalid_maintenance("maintenance participant callback is unavailable")
179                })?;
180            let role = if plugin == target_owner {
181                OwnerAuthorizedParticipantRole::TargetOwner
182            } else {
183                OwnerAuthorizedParticipantRole::DependentOwner
184            };
185            let reads = descriptor
186                .record_schemas
187                .iter()
188                .map(records::DomainRecordSchema::state_key)
189                .collect::<Vec<_>>();
190            let proposal = catch_unwind(AssertUnwindSafe(|| {
191                handler(&self.plugin_view(plugin, &reads), &request, role)
192            }))
193            .map_err(|_| {
194                CanwuError::new(
195                    ErrorCode::PluginPanicked,
196                    format!("maintenance participant {plugin} panicked"),
197                )
198            })??;
199            if proposal.plugin != *plugin || proposal.role != role {
200                return Err(invalid_maintenance(
201                    "maintenance callback returned the wrong participant identity or role",
202                ));
203            }
204            participant_drafts.push(proposal);
205        }
206        if participant_drafts
207            .iter()
208            .map(|proposal| proposal.mutations.len())
209            .sum::<usize>()
210            > MAX_OWNER_AUTHORIZED_MUTATIONS
211        {
212            return Err(invalid_maintenance(
213                "owner-authorized maintenance mutation budget is invalid",
214            ));
215        }
216        let mut draft = OwnerAuthorizedMaintenanceDraft {
217            request_id: request.request_id,
218            target: request.target,
219            requested_at: request.requested_at,
220            participants: participant_drafts,
221        };
222        draft
223            .participants
224            .sort_by(|left, right| left.plugin.cmp(&right.plugin));
225
226        let mut participants = Vec::with_capacity(draft.participants.len());
227        for mut proposal in draft.participants {
228            let descriptor = self
229                .plugins
230                .descriptors
231                .get(&proposal.plugin)
232                .ok_or_else(|| {
233                    invalid_maintenance("maintenance participant has no plugin descriptor")
234                })?;
235            let expected_role = if proposal.plugin == *target_owner {
236                OwnerAuthorizedParticipantRole::TargetOwner
237            } else {
238                OwnerAuthorizedParticipantRole::DependentOwner
239            };
240            if proposal.role != expected_role
241                || !proposal.accepted
242                || proposal.rejection_reason.is_some()
243            {
244                return Err(invalid_maintenance(
245                    "maintenance participant rejected or claimed the wrong owner role",
246                ));
247            }
248            proposal.expected_records.sort();
249            proposal.expected_records.dedup();
250            if proposal.expected_records.is_empty()
251                || proposal
252                    .expected_records
253                    .iter()
254                    .any(|expected| expected.version == 0)
255            {
256                return Err(invalid_maintenance(
257                    "maintenance participant requires exact nonzero record expectations",
258                ));
259            }
260            for expected in &proposal.expected_records {
261                let record = self
262                    .state
263                    .current
264                    .domain_records
265                    .get(&expected.record)
266                    .ok_or_else(|| {
267                        invalid_maintenance("maintenance expected record is unavailable")
268                    })?;
269                if record.version != expected.version {
270                    return Err(invalid_maintenance(
271                        "maintenance participant record expectation is stale",
272                    ));
273                }
274            }
275            for change in &proposal.mutations {
276                if !canonical_text(&change.summary) {
277                    return Err(invalid_maintenance(
278                        "maintenance mutation summary is not canonical",
279                    ));
280                }
281                let owner = self
282                    .plugins
283                    .record_schemas
284                    .get(&change.mutation.target().kind)
285                    .map(|(owner, _)| owner)
286                    .ok_or_else(|| {
287                        invalid_maintenance("maintenance mutation target has no schema owner")
288                    })?;
289                if owner != &proposal.plugin {
290                    return Err(CanwuError::new(
291                        ErrorCode::UndeclaredStateWrite,
292                        "owner-authorized proposal targets another plugin's schema",
293                    ));
294                }
295            }
296            if proposal.role == OwnerAuthorizedParticipantRole::TargetOwner
297                && !proposal.mutations.iter().any(|change| {
298                    matches!(
299                        &change.mutation,
300                        DomainRecordMutation::Update {
301                            record,
302                            expected_version,
303                        } if record.reference == draft.target.record
304                            && *expected_version == draft.target.version
305                    ) || matches!(
306                        &change.mutation,
307                        DomainRecordMutation::Retire { record, expected_version, .. }
308                            if record == &draft.target.record
309                                && *expected_version == draft.target.version
310                    ) || matches!(
311                        &change.mutation,
312                        DomainRecordMutation::Delete { record, expected_version }
313                            if record == &draft.target.record
314                                && *expected_version == draft.target.version
315                    )
316                })
317            {
318                return Err(invalid_maintenance(
319                    "target owner did not supply an exact owner-defined target mutation",
320                ));
321            }
322            let proposal_commitment =
323                participant_commitment(&proposal, &descriptor.semantic_hash, &draft.target)?;
324            participants.push(OwnerAuthorizedParticipantProposal {
325                plugin: proposal.plugin,
326                semantic_hash: descriptor.semantic_hash.clone(),
327                role: proposal.role,
328                accepted: proposal.accepted,
329                rejection_reason: proposal.rejection_reason,
330                expected_records: proposal.expected_records,
331                mutations: proposal.mutations,
332                proposal_commitment,
333            });
334        }
335        let source_domain_root = canonical_hash(
336            "canwu.owner-authorized.source-domain-root.v1",
337            self.state.current.domain_records.roots(),
338        )?;
339        let token = canonical_hash(
340            "canwu.owner-authorized.maintenance-token.v1",
341            &(
342                OWNER_AUTHORIZED_MAINTENANCE_FORMAT_VERSION,
343                &draft.request_id,
344                &draft.target,
345                draft.requested_at,
346                &source_domain_root,
347                &participants,
348            ),
349        )?;
350        let commit = VerifiedOwnerAuthorizedMaintenanceCommit {
351            format_version: OWNER_AUTHORIZED_MAINTENANCE_FORMAT_VERSION,
352            request_id: draft.request_id,
353            target: draft.target,
354            requested_at: draft.requested_at,
355            source_domain_root,
356            participants,
357            token,
358        };
359        let _ = self.apply_owner_authorized_commit_to_root(&commit)?;
360        Ok(commit)
361    }
362
363    pub(super) fn enqueue_owner_authorized_maintenance(
364        &mut self,
365        due_at: SimTime,
366        priority: i32,
367        commit: VerifiedOwnerAuthorizedMaintenanceCommit,
368    ) -> Result<IngressReceipt, CanwuError> {
369        self.ensure_runtime_ready()?;
370        self.ensure_canonical_ingress_can_start()?;
371        let _ = self.apply_owner_authorized_commit_to_root(&commit)?;
372        for record in &self.state.evidence.ingress {
373            let IngressPayload::Maintenance { request } = &record.payload else {
374                continue;
375            };
376            if let super::MaintenanceIngressRequest::OwnerAuthorized { commit: existing } =
377                request.as_ref()
378                && existing.token() == commit.token()
379            {
380                if existing == &commit && record.due_at == due_at && record.priority == priority {
381                    return Ok(IngressReceipt {
382                        ingress_id: record.id,
383                        issued_at: record.issued_at,
384                        due_at: record.due_at,
385                    });
386                }
387                return Err(CanwuError::new(
388                    ErrorCode::IdempotencyConflict,
389                    "owner-authorized maintenance token is already queued differently",
390                ));
391            }
392        }
393        self.append_ingress(
394            due_at,
395            IngressClass::ScheduledSystem,
396            priority,
397            IngressPayload::Maintenance {
398                request: Box::new(super::MaintenanceIngressRequest::OwnerAuthorized { commit }),
399            },
400            Some(CauseRef::System(
401                "canwu.core.owner-authorized-maintenance".to_owned(),
402            )),
403            false,
404        )
405    }
406
407    pub(super) fn apply_owner_authorized_maintenance(
408        &mut self,
409        commit: &VerifiedOwnerAuthorizedMaintenanceCommit,
410    ) -> Result<Vec<super::DomainRecordChange>, CanwuError> {
411        let (next, changes) = self.apply_owner_authorized_commit_to_root(commit)?;
412        self.state.current.domain_records = next;
413        self.invalidate_commitments(super::CommitmentDomains::DOMAIN_RECORDS);
414        Ok(changes)
415    }
416
417    fn apply_owner_authorized_commit_to_root(
418        &self,
419        commit: &VerifiedOwnerAuthorizedMaintenanceCommit,
420    ) -> Result<(PersistentDomainRecordStore, Vec<DomainRecordChange>), CanwuError> {
421        apply_verified_owner_authorized_commit(
422            commit,
423            &self.state.current.domain_records,
424            &self.plugins,
425            self.state.scheduler.now,
426            &|entity| runtime_entity_exists(&self.state, entity),
427        )
428    }
429}
430
431pub(super) fn apply_verified_owner_authorized_commit(
432    commit: &VerifiedOwnerAuthorizedMaintenanceCommit,
433    current: &PersistentDomainRecordStore,
434    plugins: &PluginRegistry,
435    now: SimTime,
436    core_exists: &dyn Fn(&EntityRef) -> bool,
437) -> Result<(PersistentDomainRecordStore, Vec<DomainRecordChange>), CanwuError> {
438    validate_verified_commit_authorization(commit, current, plugins)?;
439    let source_domain_root = canonical_hash(
440        "canwu.owner-authorized.source-domain-root.v1",
441        current.roots(),
442    )?;
443    if source_domain_root != commit.source_domain_root {
444        return Err(invalid_maintenance(
445            "owner-authorized maintenance source root is stale",
446        ));
447    }
448    let mut mutations = Vec::new();
449    for proposal in &commit.participants {
450        mutations.extend(
451            proposal
452                .mutations
453                .iter()
454                .map(|change| records::DomainMutationRequest {
455                    plugin: proposal.plugin.as_str(),
456                    system: OWNER_AUTHORIZED_MAINTENANCE_SYSTEM,
457                    visibility: change.visibility,
458                    mutation: &change.mutation,
459                    summary: &change.summary,
460                }),
461        );
462    }
463    records::apply_mutation_bundle_cow(
464        current,
465        &plugins.record_schemas,
466        now,
467        core_exists,
468        mutations,
469    )
470}
471
472fn validate_verified_commit_authorization(
473    commit: &VerifiedOwnerAuthorizedMaintenanceCommit,
474    current: &PersistentDomainRecordStore,
475    plugins: &PluginRegistry,
476) -> Result<(), CanwuError> {
477    validate_verified_commit_authorization_structure(commit, plugins)?;
478    validate_verified_commit_freshness(commit, current)
479}
480
481pub(super) fn validate_verified_commit_authorization_structure(
482    commit: &VerifiedOwnerAuthorizedMaintenanceCommit,
483    plugins: &PluginRegistry,
484) -> Result<(), CanwuError> {
485    validate_verified_commit_shape(commit)?;
486    if commit.target.version == 0 {
487        return Err(invalid_maintenance(
488            "maintenance target expectation must use a nonzero version",
489        ));
490    }
491    let (target_owner, _) = plugins
492        .record_schemas
493        .get(&commit.target.record.kind)
494        .ok_or_else(|| invalid_maintenance("maintenance target schema has no owner"))?;
495    let mut required = plugins
496        .maintenance_dependency_resolvers
497        .get(&commit.target.record.kind.namespace)
498        .cloned()
499        .unwrap_or_default();
500    required.insert(target_owner.clone());
501    if required.is_empty()
502        || required.len() > MAX_OWNER_AUTHORIZED_PARTICIPANTS
503        || commit.participants.len() != required.len()
504        || !commit
505            .participants
506            .iter()
507            .map(|proposal| &proposal.plugin)
508            .eq(required.iter())
509    {
510        return Err(invalid_maintenance(
511            "owner-authorized maintenance participant set is incomplete or noncanonical",
512        ));
513    }
514    let mutation_count = commit
515        .participants
516        .iter()
517        .try_fold(0_usize, |total, proposal| {
518            total.checked_add(proposal.mutations.len()).ok_or_else(|| {
519                invalid_maintenance("owner-authorized maintenance mutation budget is invalid")
520            })
521        })?;
522    if mutation_count > MAX_OWNER_AUTHORIZED_MUTATIONS {
523        return Err(invalid_maintenance(
524            "owner-authorized maintenance mutation budget is invalid",
525        ));
526    }
527    for proposal in &commit.participants {
528        let descriptor = plugins.descriptors.get(&proposal.plugin).ok_or_else(|| {
529            invalid_maintenance("maintenance participant descriptor is unavailable")
530        })?;
531        let expected_role = if proposal.plugin == *target_owner {
532            OwnerAuthorizedParticipantRole::TargetOwner
533        } else {
534            OwnerAuthorizedParticipantRole::DependentOwner
535        };
536        if !descriptor.owner_authorized_maintenance_participant
537            || descriptor.semantic_hash != proposal.semantic_hash
538            || proposal.role != expected_role
539            || !proposal.accepted
540            || proposal.rejection_reason.is_some()
541            || participant_commitment_from_verified(proposal, &commit.target)?
542                != proposal.proposal_commitment
543        {
544            return Err(invalid_maintenance(
545                "maintenance participant identity, role, or commitment changed",
546            ));
547        }
548        if proposal.expected_records.is_empty()
549            || proposal
550                .expected_records
551                .windows(2)
552                .any(|pair| pair[0] >= pair[1])
553            || proposal
554                .expected_records
555                .iter()
556                .any(|expected| expected.version == 0)
557        {
558            return Err(invalid_maintenance(
559                "maintenance participant expectations are empty or noncanonical",
560            ));
561        }
562        for change in &proposal.mutations {
563            if !canonical_text(&change.summary) {
564                return Err(invalid_maintenance(
565                    "maintenance mutation summary is not canonical",
566                ));
567            }
568            let owner = plugins
569                .record_schemas
570                .get(&change.mutation.target().kind)
571                .map(|(owner, _)| owner)
572                .ok_or_else(|| {
573                    invalid_maintenance("maintenance mutation target has no schema owner")
574                })?;
575            if owner != &proposal.plugin {
576                return Err(CanwuError::new(
577                    ErrorCode::UndeclaredStateWrite,
578                    "owner-authorized proposal targets another plugin's schema",
579                ));
580            }
581        }
582        if proposal.role == OwnerAuthorizedParticipantRole::TargetOwner
583            && !proposal.mutations.iter().any(|change| {
584                matches!(
585                    &change.mutation,
586                    DomainRecordMutation::Update {
587                        record,
588                        expected_version,
589                    } if record.reference == commit.target.record
590                        && *expected_version == commit.target.version
591                ) || matches!(
592                    &change.mutation,
593                    DomainRecordMutation::Retire { record, expected_version, .. }
594                        if record == &commit.target.record
595                            && *expected_version == commit.target.version
596                ) || matches!(
597                    &change.mutation,
598                    DomainRecordMutation::Delete { record, expected_version }
599                        if record == &commit.target.record
600                            && *expected_version == commit.target.version
601                )
602            })
603        {
604            return Err(invalid_maintenance(
605                "target owner did not supply an exact owner-defined target mutation",
606            ));
607        }
608    }
609    Ok(())
610}
611
612fn validate_verified_commit_freshness(
613    commit: &VerifiedOwnerAuthorizedMaintenanceCommit,
614    current: &PersistentDomainRecordStore,
615) -> Result<(), CanwuError> {
616    let target_record = current
617        .get(&commit.target.record)
618        .ok_or_else(|| invalid_maintenance("maintenance target record is unavailable"))?;
619    if target_record.version != commit.target.version || !target_record.is_active() {
620        return Err(invalid_maintenance(
621            "maintenance target expectation is stale or not active",
622        ));
623    }
624    for proposal in &commit.participants {
625        for expected in &proposal.expected_records {
626            if current
627                .get(&expected.record)
628                .is_none_or(|record| record.version != expected.version)
629            {
630                return Err(invalid_maintenance(
631                    "owner-authorized maintenance expectation is stale",
632                ));
633            }
634        }
635    }
636    Ok(())
637}
638
639fn participant_commitment(
640    proposal: &OwnerAuthorizedParticipantDraft,
641    semantic_hash: &str,
642    target: &OwnerAuthorizedRecordExpectation,
643) -> Result<String, CanwuError> {
644    canonical_hash(
645        "canwu.owner-authorized.participant.v1",
646        &(
647            &proposal.plugin,
648            semantic_hash,
649            proposal.role,
650            proposal.accepted,
651            &proposal.rejection_reason,
652            &proposal.expected_records,
653            &proposal.mutations,
654            target,
655        ),
656    )
657}
658
659fn participant_commitment_from_verified(
660    proposal: &OwnerAuthorizedParticipantProposal,
661    target: &OwnerAuthorizedRecordExpectation,
662) -> Result<String, CanwuError> {
663    canonical_hash(
664        "canwu.owner-authorized.participant.v1",
665        &(
666            &proposal.plugin,
667            &proposal.semantic_hash,
668            proposal.role,
669            proposal.accepted,
670            &proposal.rejection_reason,
671            &proposal.expected_records,
672            &proposal.mutations,
673            target,
674        ),
675    )
676}
677
678pub(super) fn validate_verified_commit_shape(
679    commit: &VerifiedOwnerAuthorizedMaintenanceCommit,
680) -> Result<(), CanwuError> {
681    if commit.format_version != OWNER_AUTHORIZED_MAINTENANCE_FORMAT_VERSION
682        || !canonical_text(&commit.request_id)
683        || !is_canonical_hash(&commit.source_domain_root)
684        || !is_canonical_hash(&commit.token)
685        || commit.participants.is_empty()
686        || commit.participants.len() > MAX_OWNER_AUTHORIZED_PARTICIPANTS
687        || commit
688            .participants
689            .windows(2)
690            .any(|pair| pair[0].plugin >= pair[1].plugin)
691        || commit.participants.iter().any(|proposal| {
692            !proposal.accepted
693                || proposal.rejection_reason.is_some()
694                || !is_canonical_hash(&proposal.semantic_hash)
695                || !is_canonical_hash(&proposal.proposal_commitment)
696        })
697    {
698        return Err(invalid_maintenance(
699            "owner-authorized maintenance commit is malformed or non-canonical",
700        ));
701    }
702    let expected = canonical_hash(
703        "canwu.owner-authorized.maintenance-token.v1",
704        &(
705            commit.format_version,
706            &commit.request_id,
707            &commit.target,
708            commit.requested_at,
709            &commit.source_domain_root,
710            &commit.participants,
711        ),
712    )?;
713    if expected != commit.token {
714        return Err(invalid_maintenance(
715            "owner-authorized maintenance token is inconsistent",
716        ));
717    }
718    Ok(())
719}
720
721fn invalid_maintenance(message: impl Into<String>) -> CanwuError {
722    CanwuError::new(ErrorCode::InvalidDomainRecord, message)
723}