1use std::collections::BTreeMap;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use starweft_crypto::{CryptoError, MessageSignature, StoredKeypair, canonical_json, verify_bytes};
12use starweft_id::{ActorId, ArtifactId, MessageId, NodeId, ProjectId, StopId, TaskId, VisionId};
13use thiserror::Error;
14use time::OffsetDateTime;
15
16pub const PROTOCOL_VERSION: &str = "starweft/0.1";
18
19#[derive(Debug, Error)]
21pub enum ProtocolError {
22 #[error("crypto error: {0}")]
24 Crypto(#[from] CryptoError),
25 #[error("serialization error: {0}")]
27 Serialization(#[from] serde_json::Error),
28 #[error("unsupported signature algorithm: {0}")]
30 UnsupportedAlgorithm(String),
31 #[error("message type/body mismatch")]
33 MessageTypeMismatch,
34 #[error("unknown status value: {0}")]
36 UnknownStatus(String),
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
41pub enum MsgType {
42 VisionIntent,
44 ProjectCharter,
46 ApprovalGranted,
48 ApprovalApplied,
50 CapabilityQuery,
52 CapabilityAdvertisement,
54 JoinOffer,
56 JoinAccept,
58 JoinReject,
60 TaskDelegated,
62 TaskProgress,
64 TaskResultSubmitted,
66 EvaluationIssued,
68 PublishIntentProposed,
70 PublishIntentSkipped,
72 PublishResultRecorded,
74 SnapshotRequest,
76 SnapshotResponse,
78 StopOrder,
80 StopAck,
82 StopComplete,
84}
85
86pub trait RoutedBody {
88 fn msg_type(&self) -> MsgType;
90}
91
92#[derive(Clone, Debug, Serialize, Deserialize)]
94pub struct UnsignedEnvelope<T> {
95 pub protocol: String,
97 pub msg_id: MessageId,
99 pub msg_type: MsgType,
101 pub from_actor_id: ActorId,
103 pub to_actor_id: Option<ActorId>,
105 pub vision_id: Option<VisionId>,
107 pub project_id: Option<ProjectId>,
109 pub task_id: Option<TaskId>,
111 pub lamport_ts: u64,
113 pub created_at: OffsetDateTime,
115 pub expires_at: Option<OffsetDateTime>,
117 pub body: T,
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct Envelope<T> {
124 pub protocol: String,
126 pub msg_id: MessageId,
128 pub msg_type: MsgType,
130 pub from_actor_id: ActorId,
132 pub to_actor_id: Option<ActorId>,
134 pub vision_id: Option<VisionId>,
136 pub project_id: Option<ProjectId>,
138 pub task_id: Option<TaskId>,
140 pub lamport_ts: u64,
142 pub created_at: OffsetDateTime,
144 pub expires_at: Option<OffsetDateTime>,
146 pub body: T,
148 pub signature: MessageSignature,
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize)]
154pub struct WireEnvelope {
155 pub protocol: String,
157 pub msg_id: MessageId,
159 pub msg_type: MsgType,
161 pub from_actor_id: ActorId,
163 pub to_actor_id: Option<ActorId>,
165 pub vision_id: Option<VisionId>,
167 pub project_id: Option<ProjectId>,
169 pub task_id: Option<TaskId>,
171 pub lamport_ts: u64,
173 pub created_at: OffsetDateTime,
175 pub expires_at: Option<OffsetDateTime>,
177 pub body: Value,
179 pub signature: MessageSignature,
181}
182
183#[derive(Serialize)]
184struct SignableEnvelope<'a, T> {
185 protocol: &'a str,
186 msg_id: &'a MessageId,
187 msg_type: &'a MsgType,
188 from_actor_id: &'a ActorId,
189 to_actor_id: &'a Option<ActorId>,
190 vision_id: &'a Option<VisionId>,
191 project_id: &'a Option<ProjectId>,
192 task_id: &'a Option<TaskId>,
193 lamport_ts: u64,
194 created_at: OffsetDateTime,
195 expires_at: &'a Option<OffsetDateTime>,
196 body: &'a T,
197}
198
199impl<T> UnsignedEnvelope<T>
200where
201 T: RoutedBody + Serialize,
202{
203 #[must_use]
205 pub fn new(from_actor_id: ActorId, to_actor_id: Option<ActorId>, body: T) -> Self {
206 Self {
207 protocol: PROTOCOL_VERSION.to_owned(),
208 msg_id: MessageId::generate(),
209 msg_type: body.msg_type(),
210 from_actor_id,
211 to_actor_id,
212 vision_id: None,
213 project_id: None,
214 task_id: None,
215 lamport_ts: 1,
216 created_at: OffsetDateTime::now_utc(),
217 expires_at: None,
218 body,
219 }
220 }
221
222 #[must_use]
224 pub fn with_vision_id(mut self, vision_id: VisionId) -> Self {
225 self.vision_id = Some(vision_id);
226 self
227 }
228
229 #[must_use]
231 pub fn with_project_id(mut self, project_id: ProjectId) -> Self {
232 self.project_id = Some(project_id);
233 self
234 }
235
236 #[must_use]
238 pub fn with_task_id(mut self, task_id: TaskId) -> Self {
239 self.task_id = Some(task_id);
240 self
241 }
242
243 #[must_use]
245 pub fn with_lamport_ts(mut self, lamport_ts: u64) -> Self {
246 self.lamport_ts = lamport_ts;
247 self
248 }
249
250 #[must_use]
252 pub fn with_expires_at(mut self, expires_at: OffsetDateTime) -> Self {
253 self.expires_at = Some(expires_at);
254 self
255 }
256
257 pub fn sign(self, keypair: &StoredKeypair) -> Result<Envelope<T>, ProtocolError> {
259 if self.msg_type != self.body.msg_type() {
260 return Err(ProtocolError::MessageTypeMismatch);
261 }
262
263 let signature = keypair.sign_bytes(&self.signable_bytes()?)?;
264
265 Ok(Envelope {
266 protocol: self.protocol,
267 msg_id: self.msg_id,
268 msg_type: self.msg_type,
269 from_actor_id: self.from_actor_id,
270 to_actor_id: self.to_actor_id,
271 vision_id: self.vision_id,
272 project_id: self.project_id,
273 task_id: self.task_id,
274 lamport_ts: self.lamport_ts,
275 created_at: self.created_at,
276 expires_at: self.expires_at,
277 body: self.body,
278 signature,
279 })
280 }
281
282 fn signable_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
283 Ok(canonical_json(&SignableEnvelope {
284 protocol: &self.protocol,
285 msg_id: &self.msg_id,
286 msg_type: &self.msg_type,
287 from_actor_id: &self.from_actor_id,
288 to_actor_id: &self.to_actor_id,
289 vision_id: &self.vision_id,
290 project_id: &self.project_id,
291 task_id: &self.task_id,
292 lamport_ts: self.lamport_ts,
293 created_at: self.created_at,
294 expires_at: &self.expires_at,
295 body: &self.body,
296 })?)
297 }
298}
299
300impl<T> Envelope<T>
301where
302 T: RoutedBody + Serialize,
303{
304 pub fn verify_with_key(
306 &self,
307 verifying_key: &ed25519_dalek::VerifyingKey,
308 ) -> Result<(), ProtocolError> {
309 if self.signature.alg != "ed25519" {
310 return Err(ProtocolError::UnsupportedAlgorithm(
311 self.signature.alg.clone(),
312 ));
313 }
314
315 if self.msg_type != self.body.msg_type() {
316 return Err(ProtocolError::MessageTypeMismatch);
317 }
318
319 let signable = SignableEnvelope {
320 protocol: &self.protocol,
321 msg_id: &self.msg_id,
322 msg_type: &self.msg_type,
323 from_actor_id: &self.from_actor_id,
324 to_actor_id: &self.to_actor_id,
325 vision_id: &self.vision_id,
326 project_id: &self.project_id,
327 task_id: &self.task_id,
328 lamport_ts: self.lamport_ts,
329 created_at: self.created_at,
330 expires_at: &self.expires_at,
331 body: &self.body,
332 };
333 verify_bytes(verifying_key, &canonical_json(&signable)?, &self.signature)?;
334 Ok(())
335 }
336
337 pub fn into_wire(self) -> Result<WireEnvelope, ProtocolError> {
339 Ok(WireEnvelope {
340 protocol: self.protocol,
341 msg_id: self.msg_id,
342 msg_type: self.msg_type,
343 from_actor_id: self.from_actor_id,
344 to_actor_id: self.to_actor_id,
345 vision_id: self.vision_id,
346 project_id: self.project_id,
347 task_id: self.task_id,
348 lamport_ts: self.lamport_ts,
349 created_at: self.created_at,
350 expires_at: self.expires_at,
351 body: serde_json::to_value(self.body)?,
352 signature: self.signature,
353 })
354 }
355}
356
357impl WireEnvelope {
358 pub fn decode<T>(self) -> Result<Envelope<T>, ProtocolError>
360 where
361 T: RoutedBody + for<'de> Deserialize<'de>,
362 {
363 let body: T = serde_json::from_value(self.body)?;
364 if body.msg_type() != self.msg_type {
365 return Err(ProtocolError::MessageTypeMismatch);
366 }
367 Ok(Envelope {
368 protocol: self.protocol,
369 msg_id: self.msg_id,
370 msg_type: self.msg_type,
371 from_actor_id: self.from_actor_id,
372 to_actor_id: self.to_actor_id,
373 vision_id: self.vision_id,
374 project_id: self.project_id,
375 task_id: self.task_id,
376 lamport_ts: self.lamport_ts,
377 created_at: self.created_at,
378 expires_at: self.expires_at,
379 body,
380 signature: self.signature,
381 })
382 }
383
384 pub fn verify_with_key(
386 &self,
387 verifying_key: &ed25519_dalek::VerifyingKey,
388 ) -> Result<(), ProtocolError> {
389 if self.signature.alg != "ed25519" {
390 return Err(ProtocolError::UnsupportedAlgorithm(
391 self.signature.alg.clone(),
392 ));
393 }
394
395 let signable = SignableEnvelope {
396 protocol: &self.protocol,
397 msg_id: &self.msg_id,
398 msg_type: &self.msg_type,
399 from_actor_id: &self.from_actor_id,
400 to_actor_id: &self.to_actor_id,
401 vision_id: &self.vision_id,
402 project_id: &self.project_id,
403 task_id: &self.task_id,
404 lamport_ts: self.lamport_ts,
405 created_at: self.created_at,
406 expires_at: &self.expires_at,
407 body: &self.body,
408 };
409 verify_bytes(verifying_key, &canonical_json(&signable)?, &self.signature)?;
410 Ok(())
411 }
412}
413
414#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum ProjectStatus {
418 Planning,
420 Active,
422 Stopping,
424 Stopped,
426}
427
428impl ProjectStatus {
429 #[must_use]
431 pub fn as_str(&self) -> &'static str {
432 match self {
433 Self::Planning => "planning",
434 Self::Active => "active",
435 Self::Stopping => "stopping",
436 Self::Stopped => "stopped",
437 }
438 }
439}
440
441impl std::fmt::Display for ProjectStatus {
442 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443 f.write_str(self.as_str())
444 }
445}
446
447impl FromStr for ProjectStatus {
448 type Err = ProtocolError;
449 fn from_str(s: &str) -> Result<Self, Self::Err> {
450 match s {
451 "planning" => Ok(Self::Planning),
452 "active" => Ok(Self::Active),
453 "stopping" => Ok(Self::Stopping),
454 "stopped" => Ok(Self::Stopped),
455 other => Err(ProtocolError::UnknownStatus(other.to_owned())),
456 }
457 }
458}
459
460#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
462#[serde(rename_all = "snake_case")]
463pub enum TaskStatus {
464 Queued,
466 Offered,
468 Accepted,
470 Running,
472 Submitted,
474 Completed,
476 Failed,
478 Stopping,
480 Stopped,
482}
483
484impl TaskStatus {
485 #[must_use]
487 pub fn as_str(&self) -> &'static str {
488 match self {
489 Self::Queued => "queued",
490 Self::Offered => "offered",
491 Self::Accepted => "accepted",
492 Self::Running => "running",
493 Self::Submitted => "submitted",
494 Self::Completed => "completed",
495 Self::Failed => "failed",
496 Self::Stopping => "stopping",
497 Self::Stopped => "stopped",
498 }
499 }
500
501 #[must_use]
503 pub fn is_active(&self) -> bool {
504 matches!(
505 self,
506 Self::Queued | Self::Accepted | Self::Running | Self::Stopping
507 )
508 }
509
510 #[must_use]
512 pub fn is_terminal(&self) -> bool {
513 matches!(self, Self::Completed | Self::Failed | Self::Stopped)
514 }
515}
516
517impl std::fmt::Display for TaskStatus {
518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519 f.write_str(self.as_str())
520 }
521}
522
523impl FromStr for TaskStatus {
524 type Err = ProtocolError;
525 fn from_str(s: &str) -> Result<Self, Self::Err> {
526 match s {
527 "queued" => Ok(Self::Queued),
528 "offered" => Ok(Self::Offered),
529 "accepted" => Ok(Self::Accepted),
530 "running" => Ok(Self::Running),
531 "submitted" => Ok(Self::Submitted),
532 "completed" => Ok(Self::Completed),
533 "failed" => Ok(Self::Failed),
534 "stopping" => Ok(Self::Stopping),
535 "stopped" => Ok(Self::Stopped),
536 other => Err(ProtocolError::UnknownStatus(other.to_owned())),
537 }
538 }
539}
540
541#[derive(Clone, Debug, Default, Serialize, Deserialize)]
543pub struct VisionConstraints {
544 pub budget_mode: Option<String>,
546 pub allow_external_agents: Option<bool>,
548 pub human_intervention: Option<String>,
550 #[serde(flatten)]
552 pub extra: BTreeMap<String, Value>,
553}
554
555#[derive(Clone, Debug, Serialize, Deserialize)]
557pub struct VisionIntent {
558 pub title: String,
560 pub raw_vision_text: String,
562 pub constraints: VisionConstraints,
564}
565
566impl RoutedBody for VisionIntent {
567 fn msg_type(&self) -> MsgType {
568 MsgType::VisionIntent
569 }
570}
571
572#[derive(Clone, Debug, Serialize, Deserialize)]
574pub struct ParticipantPolicy {
575 pub external_agents_allowed: bool,
577}
578
579#[derive(Clone, Debug, Serialize, Deserialize)]
581pub struct EvaluationPolicy {
582 pub quality_weight: f32,
584 pub speed_weight: f32,
586 pub reliability_weight: f32,
588 pub alignment_weight: f32,
590}
591
592#[derive(Clone, Debug, Serialize, Deserialize)]
594pub struct ProjectCharter {
595 pub project_id: ProjectId,
597 pub vision_id: VisionId,
599 pub principal_actor_id: ActorId,
601 pub owner_actor_id: ActorId,
603 pub title: String,
605 pub objective: String,
607 pub stop_authority_actor_id: ActorId,
609 pub participant_policy: ParticipantPolicy,
611 pub evaluation_policy: EvaluationPolicy,
613}
614
615impl RoutedBody for ProjectCharter {
616 fn msg_type(&self) -> MsgType {
617 MsgType::ProjectCharter
618 }
619}
620
621#[derive(Clone, Debug, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case")]
624pub enum ApprovalScopeType {
625 Project,
627 Task,
629}
630
631#[derive(Clone, Debug, Serialize, Deserialize)]
633pub struct ApprovalGranted {
634 pub scope_type: ApprovalScopeType,
636 pub scope_id: String,
638 pub approved_at: OffsetDateTime,
640}
641
642impl RoutedBody for ApprovalGranted {
643 fn msg_type(&self) -> MsgType {
644 MsgType::ApprovalGranted
645 }
646}
647
648#[derive(Clone, Debug, Serialize, Deserialize)]
650pub struct ApprovalApplied {
651 pub scope_type: ApprovalScopeType,
653 pub scope_id: String,
655 pub approval_granted_msg_id: MessageId,
657 pub approval_updated: bool,
659 pub resumed_task_ids: Vec<String>,
661 pub dispatched: bool,
663 pub applied_at: OffsetDateTime,
665}
666
667impl RoutedBody for ApprovalApplied {
668 fn msg_type(&self) -> MsgType {
669 MsgType::ApprovalApplied
670 }
671}
672
673#[derive(Clone, Debug, Serialize, Deserialize)]
675pub struct CapabilityQuery {
676 pub node_id: NodeId,
678 pub public_key: String,
680 pub stop_public_key: Option<String>,
682 pub capabilities: Vec<String>,
684 pub listen_addresses: Vec<String>,
686 pub requested_at: OffsetDateTime,
688}
689
690impl RoutedBody for CapabilityQuery {
691 fn msg_type(&self) -> MsgType {
692 MsgType::CapabilityQuery
693 }
694}
695
696#[derive(Clone, Debug, Serialize, Deserialize)]
698pub struct CapabilityAdvertisement {
699 pub node_id: NodeId,
701 pub public_key: String,
703 pub stop_public_key: Option<String>,
705 pub capabilities: Vec<String>,
707 pub listen_addresses: Vec<String>,
709 pub advertised_at: OffsetDateTime,
711}
712
713impl RoutedBody for CapabilityAdvertisement {
714 fn msg_type(&self) -> MsgType {
715 MsgType::CapabilityAdvertisement
716 }
717}
718
719#[derive(Clone, Debug, Serialize, Deserialize)]
721pub struct JoinOffer {
722 pub required_capabilities: Vec<String>,
724 pub task_outline: String,
726 pub expected_duration_sec: u64,
728}
729
730impl RoutedBody for JoinOffer {
731 fn msg_type(&self) -> MsgType {
732 MsgType::JoinOffer
733 }
734}
735
736#[derive(Clone, Debug, Serialize, Deserialize)]
738pub struct JoinAccept {
739 pub accepted: bool,
741 pub capabilities_confirmed: Vec<String>,
743}
744
745impl RoutedBody for JoinAccept {
746 fn msg_type(&self) -> MsgType {
747 MsgType::JoinAccept
748 }
749}
750
751#[derive(Clone, Debug, Serialize, Deserialize)]
753pub struct JoinReject {
754 pub accepted: bool,
756 pub reason: String,
758}
759
760impl RoutedBody for JoinReject {
761 fn msg_type(&self) -> MsgType {
762 MsgType::JoinReject
763 }
764}
765
766#[derive(Clone, Debug, Serialize, Deserialize)]
768pub struct TaskDelegated {
769 pub parent_task_id: Option<TaskId>,
771 pub title: String,
773 pub description: String,
775 pub objective: String,
777 pub required_capability: String,
779 pub input_payload: Value,
781 pub expected_output_schema: Value,
783}
784
785impl RoutedBody for TaskDelegated {
786 fn msg_type(&self) -> MsgType {
787 MsgType::TaskDelegated
788 }
789}
790
791#[derive(Clone, Debug, Serialize, Deserialize)]
793pub struct TaskProgress {
794 pub progress: f32,
796 pub message: String,
798 pub updated_at: OffsetDateTime,
800}
801
802impl RoutedBody for TaskProgress {
803 fn msg_type(&self) -> MsgType {
804 MsgType::TaskProgress
805 }
806}
807
808#[derive(Clone, Debug, Serialize, Deserialize)]
810pub struct ArtifactEncryption {
811 pub mode: String,
813 pub recipients: Vec<ActorId>,
815}
816
817#[derive(Clone, Debug, Serialize, Deserialize)]
819pub struct ArtifactRef {
820 pub artifact_id: ArtifactId,
822 pub scheme: String,
824 pub uri: String,
826 pub sha256: Option<String>,
828 pub size: Option<u64>,
830 pub encryption: Option<ArtifactEncryption>,
832}
833
834#[derive(Clone, Debug, Serialize, Deserialize)]
836#[serde(rename_all = "snake_case")]
837pub enum TaskExecutionStatus {
838 Completed,
840 Failed,
842 Stopped,
844}
845
846#[derive(Clone, Debug, Serialize, Deserialize)]
848pub struct TaskResultSubmitted {
849 pub status: TaskExecutionStatus,
851 pub summary: String,
853 pub output_payload: Value,
855 pub artifact_refs: Vec<ArtifactRef>,
857 pub started_at: OffsetDateTime,
859 pub finished_at: OffsetDateTime,
861}
862
863impl RoutedBody for TaskResultSubmitted {
864 fn msg_type(&self) -> MsgType {
865 MsgType::TaskResultSubmitted
866 }
867}
868
869#[derive(Clone, Debug, Serialize, Deserialize)]
871pub struct EvaluationIssued {
872 pub subject_actor_id: ActorId,
874 pub scores: BTreeMap<String, f32>,
876 pub comment: String,
878}
879
880impl RoutedBody for EvaluationIssued {
881 fn msg_type(&self) -> MsgType {
882 MsgType::EvaluationIssued
883 }
884}
885
886#[derive(Clone, Debug, Serialize, Deserialize)]
888pub struct PublishIntentProposed {
889 pub scope_type: String,
891 pub scope_id: String,
893 pub target: String,
895 pub reason: String,
897 pub summary: String,
899 pub context: Value,
901 pub proposed_at: OffsetDateTime,
903}
904
905impl RoutedBody for PublishIntentProposed {
906 fn msg_type(&self) -> MsgType {
907 MsgType::PublishIntentProposed
908 }
909}
910
911#[derive(Clone, Debug, Serialize, Deserialize)]
913pub struct PublishIntentSkipped {
914 pub scope_type: String,
916 pub scope_id: String,
918 pub target: String,
920 pub reason: String,
922 pub context: Value,
924 pub skipped_at: OffsetDateTime,
926}
927
928impl RoutedBody for PublishIntentSkipped {
929 fn msg_type(&self) -> MsgType {
930 MsgType::PublishIntentSkipped
931 }
932}
933
934#[derive(Clone, Debug, Serialize, Deserialize)]
936pub struct PublishResultRecorded {
937 pub scope_type: String,
939 pub scope_id: String,
941 pub target: String,
943 pub status: String,
945 pub location: Option<String>,
947 pub detail: String,
949 pub result_payload: Value,
951 pub recorded_at: OffsetDateTime,
953}
954
955impl RoutedBody for PublishResultRecorded {
956 fn msg_type(&self) -> MsgType {
957 MsgType::PublishResultRecorded
958 }
959}
960
961#[derive(Clone, Debug, Serialize, Deserialize)]
963#[serde(rename_all = "snake_case")]
964pub enum SnapshotScopeType {
965 Project,
967 Task,
969}
970
971#[derive(Clone, Debug, Serialize, Deserialize)]
973pub struct SnapshotRequest {
974 pub scope_type: SnapshotScopeType,
976 pub scope_id: String,
978}
979
980impl RoutedBody for SnapshotRequest {
981 fn msg_type(&self) -> MsgType {
982 MsgType::SnapshotRequest
983 }
984}
985
986#[derive(Clone, Debug, Serialize, Deserialize)]
988pub struct SnapshotResponse {
989 pub scope_type: SnapshotScopeType,
991 pub scope_id: String,
993 pub snapshot: Value,
995}
996
997impl RoutedBody for SnapshotResponse {
998 fn msg_type(&self) -> MsgType {
999 MsgType::SnapshotResponse
1000 }
1001}
1002
1003#[derive(Clone, Debug, Serialize, Deserialize)]
1005#[serde(rename_all = "snake_case")]
1006pub enum StopScopeType {
1007 Project,
1009 TaskTree,
1011}
1012
1013#[derive(Clone, Debug, Serialize, Deserialize)]
1015pub struct StopOrder {
1016 pub stop_id: StopId,
1018 pub scope_type: StopScopeType,
1020 pub scope_id: String,
1022 pub reason_code: String,
1024 pub reason_text: String,
1026 pub issued_at: OffsetDateTime,
1028}
1029
1030impl RoutedBody for StopOrder {
1031 fn msg_type(&self) -> MsgType {
1032 MsgType::StopOrder
1033 }
1034}
1035
1036#[derive(Clone, Debug, Serialize, Deserialize)]
1038#[serde(rename_all = "snake_case")]
1039pub enum StopAckState {
1040 Stopping,
1042}
1043
1044#[derive(Clone, Debug, Serialize, Deserialize)]
1046pub struct StopAck {
1047 pub stop_id: StopId,
1049 pub actor_id: ActorId,
1051 pub ack_state: StopAckState,
1053 pub acked_at: OffsetDateTime,
1055}
1056
1057impl RoutedBody for StopAck {
1058 fn msg_type(&self) -> MsgType {
1059 MsgType::StopAck
1060 }
1061}
1062
1063#[derive(Clone, Debug, Serialize, Deserialize)]
1065#[serde(rename_all = "snake_case")]
1066pub enum StopFinalState {
1067 Stopped,
1069}
1070
1071#[derive(Clone, Debug, Serialize, Deserialize)]
1073pub struct StopComplete {
1074 pub stop_id: StopId,
1076 pub actor_id: ActorId,
1078 pub final_state: StopFinalState,
1080 pub completed_at: OffsetDateTime,
1082}
1083
1084impl RoutedBody for StopComplete {
1085 fn msg_type(&self) -> MsgType {
1086 MsgType::StopComplete
1087 }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092 use super::*;
1093 use starweft_crypto::StoredKeypair;
1094 use starweft_id::ActorId;
1095
1096 #[test]
1097 fn signed_envelope_round_trip_verifies() {
1098 let keypair = StoredKeypair::generate();
1099 let actor_id = ActorId::generate();
1100 let envelope = UnsignedEnvelope::new(
1101 actor_id,
1102 None,
1103 VisionIntent {
1104 title: "vision".to_owned(),
1105 raw_vision_text: "build something".to_owned(),
1106 constraints: VisionConstraints::default(),
1107 },
1108 )
1109 .sign(&keypair)
1110 .expect("sign");
1111
1112 envelope
1113 .verify_with_key(&keypair.verifying_key().expect("verifying key"))
1114 .expect("verify");
1115 }
1116
1117 #[test]
1118 fn wire_envelope_round_trip_verifies() {
1119 let keypair = StoredKeypair::generate();
1120 let actor_id = ActorId::generate();
1121 let wire = UnsignedEnvelope::new(
1122 actor_id,
1123 None,
1124 VisionIntent {
1125 title: "vision".to_owned(),
1126 raw_vision_text: "build something".to_owned(),
1127 constraints: VisionConstraints::default(),
1128 },
1129 )
1130 .sign(&keypair)
1131 .expect("sign")
1132 .into_wire()
1133 .expect("wire");
1134
1135 wire.verify_with_key(&keypair.verifying_key().expect("verifying key"))
1136 .expect("verify");
1137 }
1138}