Skip to main content

starweft_protocol/
lib.rs

1//! Protocol message types, envelopes, and signature verification for Starweft.
2//!
3//! Defines the complete set of typed message bodies exchanged between agents,
4//! the signed envelope format, wire serialization, and domain status enums.
5
6use 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
16/// Current protocol version string used in all envelopes.
17pub const PROTOCOL_VERSION: &str = "starweft/0.1";
18
19/// Errors that can occur during protocol-level operations.
20#[derive(Debug, Error)]
21pub enum ProtocolError {
22    /// An underlying cryptographic operation failed.
23    #[error("crypto error: {0}")]
24    Crypto(#[from] CryptoError),
25    /// JSON serialization or deserialization failed.
26    #[error("serialization error: {0}")]
27    Serialization(#[from] serde_json::Error),
28    /// The signature uses an algorithm other than Ed25519.
29    #[error("unsupported signature algorithm: {0}")]
30    UnsupportedAlgorithm(String),
31    /// The `msg_type` field does not match the body's declared type.
32    #[error("message type/body mismatch")]
33    MessageTypeMismatch,
34    /// A status string could not be parsed into the expected enum.
35    #[error("unknown status value: {0}")]
36    UnknownStatus(String),
37}
38
39/// Discriminator for protocol message types.
40#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
41pub enum MsgType {
42    /// A new vision intent from a principal.
43    VisionIntent,
44    /// A project charter establishing a project from a vision.
45    ProjectCharter,
46    /// Approval granted for a scope (project or task).
47    ApprovalGranted,
48    /// Confirmation that an approval has been applied.
49    ApprovalApplied,
50    /// Query for available capabilities from a node.
51    CapabilityQuery,
52    /// Advertisement of a node's capabilities.
53    CapabilityAdvertisement,
54    /// Offer to join a project with required capabilities.
55    JoinOffer,
56    /// Acceptance of a join offer.
57    JoinAccept,
58    /// Rejection of a join offer.
59    JoinReject,
60    /// A task delegated to a worker agent.
61    TaskDelegated,
62    /// Progress update for a running task.
63    TaskProgress,
64    /// Final result submitted for a completed/failed task.
65    TaskResultSubmitted,
66    /// Evaluation certificate issued for a task result.
67    EvaluationIssued,
68    /// Proposal to publish results to an external target.
69    PublishIntentProposed,
70    /// Notification that a publish intent was skipped.
71    PublishIntentSkipped,
72    /// Record of a completed publish operation.
73    PublishResultRecorded,
74    /// Request for a state snapshot.
75    SnapshotRequest,
76    /// Response containing a state snapshot.
77    SnapshotResponse,
78    /// Order to stop a project or task tree.
79    StopOrder,
80    /// Acknowledgment that a stop order was received.
81    StopAck,
82    /// Confirmation that a stop has fully completed.
83    StopComplete,
84}
85
86/// Trait implemented by all message body types to declare their [`MsgType`].
87pub trait RoutedBody {
88    /// Returns the message type discriminator for this body.
89    fn msg_type(&self) -> MsgType;
90}
91
92/// An envelope that has not yet been signed.
93#[derive(Clone, Debug, Serialize, Deserialize)]
94pub struct UnsignedEnvelope<T> {
95    /// Protocol version string.
96    pub protocol: String,
97    /// Unique message identifier.
98    pub msg_id: MessageId,
99    /// Type discriminator matching the body.
100    pub msg_type: MsgType,
101    /// Actor that created this message.
102    pub from_actor_id: ActorId,
103    /// Intended recipient actor, if directed.
104    pub to_actor_id: Option<ActorId>,
105    /// Associated vision, if any.
106    pub vision_id: Option<VisionId>,
107    /// Associated project, if any.
108    pub project_id: Option<ProjectId>,
109    /// Associated task, if any.
110    pub task_id: Option<TaskId>,
111    /// Lamport logical timestamp for causal ordering.
112    pub lamport_ts: u64,
113    /// Wall-clock creation time.
114    pub created_at: OffsetDateTime,
115    /// Optional expiration time for this message.
116    pub expires_at: Option<OffsetDateTime>,
117    /// The typed message body.
118    pub body: T,
119}
120
121/// A signed envelope carrying a typed message body.
122#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct Envelope<T> {
124    /// Protocol version string.
125    pub protocol: String,
126    /// Unique message identifier.
127    pub msg_id: MessageId,
128    /// Type discriminator matching the body.
129    pub msg_type: MsgType,
130    /// Actor that created this message.
131    pub from_actor_id: ActorId,
132    /// Intended recipient actor, if directed.
133    pub to_actor_id: Option<ActorId>,
134    /// Associated vision, if any.
135    pub vision_id: Option<VisionId>,
136    /// Associated project, if any.
137    pub project_id: Option<ProjectId>,
138    /// Associated task, if any.
139    pub task_id: Option<TaskId>,
140    /// Lamport logical timestamp for causal ordering.
141    pub lamport_ts: u64,
142    /// Wall-clock creation time.
143    pub created_at: OffsetDateTime,
144    /// Optional expiration time for this message.
145    pub expires_at: Option<OffsetDateTime>,
146    /// The typed message body.
147    pub body: T,
148    /// Ed25519 signature over the canonical envelope content.
149    pub signature: MessageSignature,
150}
151
152/// A wire-format envelope where the body is untyped JSON for transport.
153#[derive(Clone, Debug, Serialize, Deserialize)]
154pub struct WireEnvelope {
155    /// Protocol version string.
156    pub protocol: String,
157    /// Unique message identifier.
158    pub msg_id: MessageId,
159    /// Type discriminator for the body.
160    pub msg_type: MsgType,
161    /// Actor that created this message.
162    pub from_actor_id: ActorId,
163    /// Intended recipient actor, if directed.
164    pub to_actor_id: Option<ActorId>,
165    /// Associated vision, if any.
166    pub vision_id: Option<VisionId>,
167    /// Associated project, if any.
168    pub project_id: Option<ProjectId>,
169    /// Associated task, if any.
170    pub task_id: Option<TaskId>,
171    /// Lamport logical timestamp for causal ordering.
172    pub lamport_ts: u64,
173    /// Wall-clock creation time.
174    pub created_at: OffsetDateTime,
175    /// Optional expiration time for this message.
176    pub expires_at: Option<OffsetDateTime>,
177    /// Untyped JSON body for wire transport.
178    pub body: Value,
179    /// Ed25519 signature over the canonical envelope content.
180    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    /// Creates a new unsigned envelope with auto-generated message ID and timestamp.
204    #[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    /// Sets the vision ID on this envelope.
223    #[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    /// Sets the project ID on this envelope.
230    #[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    /// Sets the task ID on this envelope.
237    #[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    /// Sets the Lamport logical timestamp on this envelope.
244    #[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    /// Sets the expiration time on this envelope.
251    #[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    /// Signs this envelope with the given keypair, producing a signed [`Envelope`].
258    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    /// Verifies the envelope's signature against the given public key.
305    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    /// Converts this typed envelope into a wire-format envelope with untyped JSON body.
338    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    /// Decodes the wire envelope's JSON body into a typed [`Envelope`].
359    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    /// Verifies the wire envelope's signature against the given public key.
385    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/// Lifecycle status of a project.
415#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum ProjectStatus {
418    /// Project is being planned and tasks have not started.
419    Planning,
420    /// Project is actively executing tasks.
421    Active,
422    /// A stop order has been issued; tasks are draining.
423    Stopping,
424    /// All tasks have been stopped and the project is terminated.
425    Stopped,
426}
427
428impl ProjectStatus {
429    /// Returns the string representation of this status.
430    #[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/// Lifecycle status of a task.
461#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
462#[serde(rename_all = "snake_case")]
463pub enum TaskStatus {
464    /// Task is queued and awaiting assignment.
465    Queued,
466    /// Task has been offered to a worker.
467    Offered,
468    /// Worker accepted the task.
469    Accepted,
470    /// Task is actively being executed.
471    Running,
472    /// Worker submitted a result, pending evaluation.
473    Submitted,
474    /// Task completed successfully.
475    Completed,
476    /// Task execution failed.
477    Failed,
478    /// A stop order is being applied to this task.
479    Stopping,
480    /// Task was stopped before completion.
481    Stopped,
482}
483
484impl TaskStatus {
485    /// Returns the string representation of this status.
486    #[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    /// Returns `true` if the task is in a non-terminal, non-submitted state.
502    #[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    /// Returns `true` if the task has reached a final state.
511    #[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/// Constraints applied to a vision that guide task planning and execution.
542#[derive(Clone, Debug, Default, Serialize, Deserialize)]
543pub struct VisionConstraints {
544    /// Budget mode (e.g. `"minimal"`, `"standard"`).
545    pub budget_mode: Option<String>,
546    /// Whether external agents may participate.
547    pub allow_external_agents: Option<bool>,
548    /// Human intervention policy (e.g. `"required"`, `"none"`).
549    pub human_intervention: Option<String>,
550    /// Additional constraint key-value pairs.
551    #[serde(flatten)]
552    pub extra: BTreeMap<String, Value>,
553}
554
555/// A vision intent submitted by a principal to initiate work.
556#[derive(Clone, Debug, Serialize, Deserialize)]
557pub struct VisionIntent {
558    /// Short human-readable title for the vision.
559    pub title: String,
560    /// Raw free-form vision text to be decomposed into tasks.
561    pub raw_vision_text: String,
562    /// Constraints governing how this vision should be executed.
563    pub constraints: VisionConstraints,
564}
565
566impl RoutedBody for VisionIntent {
567    fn msg_type(&self) -> MsgType {
568        MsgType::VisionIntent
569    }
570}
571
572/// Policy controlling which agents may participate in a project.
573#[derive(Clone, Debug, Serialize, Deserialize)]
574pub struct ParticipantPolicy {
575    /// Whether agents outside the local node may join.
576    pub external_agents_allowed: bool,
577}
578
579/// Weights for multi-dimensional task evaluation scoring.
580#[derive(Clone, Debug, Serialize, Deserialize)]
581pub struct EvaluationPolicy {
582    /// Weight for the quality dimension.
583    pub quality_weight: f32,
584    /// Weight for the speed dimension.
585    pub speed_weight: f32,
586    /// Weight for the reliability dimension.
587    pub reliability_weight: f32,
588    /// Weight for the alignment dimension.
589    pub alignment_weight: f32,
590}
591
592/// A project charter establishing a new project from a vision.
593#[derive(Clone, Debug, Serialize, Deserialize)]
594pub struct ProjectCharter {
595    /// Unique project identifier.
596    pub project_id: ProjectId,
597    /// The vision that spawned this project.
598    pub vision_id: VisionId,
599    /// The principal (human) who initiated the vision.
600    pub principal_actor_id: ActorId,
601    /// The agent that owns and orchestrates the project.
602    pub owner_actor_id: ActorId,
603    /// Short project title.
604    pub title: String,
605    /// High-level project objective.
606    pub objective: String,
607    /// Actor authorized to issue stop orders.
608    pub stop_authority_actor_id: ActorId,
609    /// Policy for agent participation.
610    pub participant_policy: ParticipantPolicy,
611    /// Policy for task result evaluation.
612    pub evaluation_policy: EvaluationPolicy,
613}
614
615impl RoutedBody for ProjectCharter {
616    fn msg_type(&self) -> MsgType {
617        MsgType::ProjectCharter
618    }
619}
620
621/// The scope an approval applies to.
622#[derive(Clone, Debug, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case")]
624pub enum ApprovalScopeType {
625    /// Approval for an entire project.
626    Project,
627    /// Approval for a specific task.
628    Task,
629}
630
631/// Notification that an approval has been granted.
632#[derive(Clone, Debug, Serialize, Deserialize)]
633pub struct ApprovalGranted {
634    /// Whether this approval is for a project or task.
635    pub scope_type: ApprovalScopeType,
636    /// The ID of the approved project or task.
637    pub scope_id: String,
638    /// When the approval was granted.
639    pub approved_at: OffsetDateTime,
640}
641
642impl RoutedBody for ApprovalGranted {
643    fn msg_type(&self) -> MsgType {
644        MsgType::ApprovalGranted
645    }
646}
647
648/// Confirmation that an approval was applied and tasks may have been resumed.
649#[derive(Clone, Debug, Serialize, Deserialize)]
650pub struct ApprovalApplied {
651    /// The scope this approval applies to.
652    pub scope_type: ApprovalScopeType,
653    /// The ID of the approved project or task.
654    pub scope_id: String,
655    /// Reference to the original approval message.
656    pub approval_granted_msg_id: MessageId,
657    /// Whether the approval state was updated (vs already applied).
658    pub approval_updated: bool,
659    /// Task IDs that were resumed as a result.
660    pub resumed_task_ids: Vec<String>,
661    /// Whether task dispatching occurred after this approval.
662    pub dispatched: bool,
663    /// When the approval was applied.
664    pub applied_at: OffsetDateTime,
665}
666
667impl RoutedBody for ApprovalApplied {
668    fn msg_type(&self) -> MsgType {
669        MsgType::ApprovalApplied
670    }
671}
672
673/// A query announcing a node's identity and requesting peer capabilities.
674#[derive(Clone, Debug, Serialize, Deserialize)]
675pub struct CapabilityQuery {
676    /// The querying node's identifier.
677    pub node_id: NodeId,
678    /// Base64-encoded public key for message verification.
679    pub public_key: String,
680    /// Optional separate public key for stop order verification.
681    pub stop_public_key: Option<String>,
682    /// Capabilities this node offers.
683    pub capabilities: Vec<String>,
684    /// Network addresses where this node can be reached.
685    pub listen_addresses: Vec<String>,
686    /// When this query was created.
687    pub requested_at: OffsetDateTime,
688}
689
690impl RoutedBody for CapabilityQuery {
691    fn msg_type(&self) -> MsgType {
692        MsgType::CapabilityQuery
693    }
694}
695
696/// A node's response advertising its identity and capabilities.
697#[derive(Clone, Debug, Serialize, Deserialize)]
698pub struct CapabilityAdvertisement {
699    /// The advertising node's identifier.
700    pub node_id: NodeId,
701    /// Base64-encoded public key for message verification.
702    pub public_key: String,
703    /// Optional separate public key for stop order verification.
704    pub stop_public_key: Option<String>,
705    /// Capabilities this node offers.
706    pub capabilities: Vec<String>,
707    /// Network addresses where this node can be reached.
708    pub listen_addresses: Vec<String>,
709    /// When this advertisement was created.
710    pub advertised_at: OffsetDateTime,
711}
712
713impl RoutedBody for CapabilityAdvertisement {
714    fn msg_type(&self) -> MsgType {
715        MsgType::CapabilityAdvertisement
716    }
717}
718
719/// An offer from a project owner to a worker to join and execute tasks.
720#[derive(Clone, Debug, Serialize, Deserialize)]
721pub struct JoinOffer {
722    /// Capabilities the worker must possess.
723    pub required_capabilities: Vec<String>,
724    /// Brief outline of the work to be done.
725    pub task_outline: String,
726    /// Estimated duration in seconds.
727    pub expected_duration_sec: u64,
728}
729
730impl RoutedBody for JoinOffer {
731    fn msg_type(&self) -> MsgType {
732        MsgType::JoinOffer
733    }
734}
735
736/// A worker's acceptance of a join offer.
737#[derive(Clone, Debug, Serialize, Deserialize)]
738pub struct JoinAccept {
739    /// Always `true` for an acceptance.
740    pub accepted: bool,
741    /// The capabilities the worker confirms it can provide.
742    pub capabilities_confirmed: Vec<String>,
743}
744
745impl RoutedBody for JoinAccept {
746    fn msg_type(&self) -> MsgType {
747        MsgType::JoinAccept
748    }
749}
750
751/// A worker's rejection of a join offer.
752#[derive(Clone, Debug, Serialize, Deserialize)]
753pub struct JoinReject {
754    /// Always `false` for a rejection.
755    pub accepted: bool,
756    /// Reason for rejecting the offer.
757    pub reason: String,
758}
759
760impl RoutedBody for JoinReject {
761    fn msg_type(&self) -> MsgType {
762        MsgType::JoinReject
763    }
764}
765
766/// A task delegated from the project owner to a worker agent.
767#[derive(Clone, Debug, Serialize, Deserialize)]
768pub struct TaskDelegated {
769    /// Parent task ID if this is a sub-task.
770    pub parent_task_id: Option<TaskId>,
771    /// Short task title.
772    pub title: String,
773    /// Detailed task description.
774    pub description: String,
775    /// The objective this task must fulfill.
776    pub objective: String,
777    /// Capability required to execute this task.
778    pub required_capability: String,
779    /// Structured input data for the worker.
780    pub input_payload: Value,
781    /// JSON schema describing the expected output format.
782    pub expected_output_schema: Value,
783}
784
785impl RoutedBody for TaskDelegated {
786    fn msg_type(&self) -> MsgType {
787        MsgType::TaskDelegated
788    }
789}
790
791/// A progress update from a worker for a running task.
792#[derive(Clone, Debug, Serialize, Deserialize)]
793pub struct TaskProgress {
794    /// Progress fraction (0.0 to 1.0).
795    pub progress: f32,
796    /// Human-readable progress message.
797    pub message: String,
798    /// When this update was recorded.
799    pub updated_at: OffsetDateTime,
800}
801
802impl RoutedBody for TaskProgress {
803    fn msg_type(&self) -> MsgType {
804        MsgType::TaskProgress
805    }
806}
807
808/// Encryption metadata for an artifact.
809#[derive(Clone, Debug, Serialize, Deserialize)]
810pub struct ArtifactEncryption {
811    /// Encryption mode (e.g. `"aes-256-gcm"`).
812    pub mode: String,
813    /// Actors who can decrypt this artifact.
814    pub recipients: Vec<ActorId>,
815}
816
817/// A reference to a stored artifact produced by a task.
818#[derive(Clone, Debug, Serialize, Deserialize)]
819pub struct ArtifactRef {
820    /// Unique artifact identifier.
821    pub artifact_id: ArtifactId,
822    /// Storage scheme (e.g. `"file"`, `"s3"`).
823    pub scheme: String,
824    /// URI pointing to the artifact location.
825    pub uri: String,
826    /// SHA-256 hash of the artifact content.
827    pub sha256: Option<String>,
828    /// Size of the artifact in bytes.
829    pub size: Option<u64>,
830    /// Encryption metadata, if the artifact is encrypted.
831    pub encryption: Option<ArtifactEncryption>,
832}
833
834/// Outcome status of a task execution.
835#[derive(Clone, Debug, Serialize, Deserialize)]
836#[serde(rename_all = "snake_case")]
837pub enum TaskExecutionStatus {
838    /// Task finished successfully.
839    Completed,
840    /// Task execution failed.
841    Failed,
842    /// Task was stopped before completion.
843    Stopped,
844}
845
846/// A task result submitted by a worker after execution.
847#[derive(Clone, Debug, Serialize, Deserialize)]
848pub struct TaskResultSubmitted {
849    /// Outcome status of the execution.
850    pub status: TaskExecutionStatus,
851    /// Human-readable summary of what was accomplished.
852    pub summary: String,
853    /// Structured output data.
854    pub output_payload: Value,
855    /// References to produced artifacts.
856    pub artifact_refs: Vec<ArtifactRef>,
857    /// When execution began.
858    pub started_at: OffsetDateTime,
859    /// When execution ended.
860    pub finished_at: OffsetDateTime,
861}
862
863impl RoutedBody for TaskResultSubmitted {
864    fn msg_type(&self) -> MsgType {
865        MsgType::TaskResultSubmitted
866    }
867}
868
869/// An evaluation certificate issued for a task result.
870#[derive(Clone, Debug, Serialize, Deserialize)]
871pub struct EvaluationIssued {
872    /// The actor whose work is being evaluated.
873    pub subject_actor_id: ActorId,
874    /// Per-dimension scores (e.g. quality, speed, reliability, alignment).
875    pub scores: BTreeMap<String, f32>,
876    /// Evaluator's comment or summary.
877    pub comment: String,
878}
879
880impl RoutedBody for EvaluationIssued {
881    fn msg_type(&self) -> MsgType {
882        MsgType::EvaluationIssued
883    }
884}
885
886/// A proposal to publish results to an external target.
887#[derive(Clone, Debug, Serialize, Deserialize)]
888pub struct PublishIntentProposed {
889    /// Scope type (e.g. `"project"`, `"task"`).
890    pub scope_type: String,
891    /// ID of the scoped entity.
892    pub scope_id: String,
893    /// Publish target identifier.
894    pub target: String,
895    /// Reason for publishing.
896    pub reason: String,
897    /// Summary of what will be published.
898    pub summary: String,
899    /// Additional context data.
900    pub context: Value,
901    /// When this proposal was created.
902    pub proposed_at: OffsetDateTime,
903}
904
905impl RoutedBody for PublishIntentProposed {
906    fn msg_type(&self) -> MsgType {
907        MsgType::PublishIntentProposed
908    }
909}
910
911/// Notification that a publish intent was skipped.
912#[derive(Clone, Debug, Serialize, Deserialize)]
913pub struct PublishIntentSkipped {
914    /// Scope type (e.g. `"project"`, `"task"`).
915    pub scope_type: String,
916    /// ID of the scoped entity.
917    pub scope_id: String,
918    /// Publish target identifier.
919    pub target: String,
920    /// Reason for skipping.
921    pub reason: String,
922    /// Additional context data.
923    pub context: Value,
924    /// When this skip was recorded.
925    pub skipped_at: OffsetDateTime,
926}
927
928impl RoutedBody for PublishIntentSkipped {
929    fn msg_type(&self) -> MsgType {
930        MsgType::PublishIntentSkipped
931    }
932}
933
934/// Record of a completed publish operation.
935#[derive(Clone, Debug, Serialize, Deserialize)]
936pub struct PublishResultRecorded {
937    /// Scope type (e.g. `"project"`, `"task"`).
938    pub scope_type: String,
939    /// ID of the scoped entity.
940    pub scope_id: String,
941    /// Publish target identifier.
942    pub target: String,
943    /// Outcome status of the publish operation.
944    pub status: String,
945    /// Published artifact location, if applicable.
946    pub location: Option<String>,
947    /// Detail about the publish result.
948    pub detail: String,
949    /// Structured result payload.
950    pub result_payload: Value,
951    /// When this result was recorded.
952    pub recorded_at: OffsetDateTime,
953}
954
955impl RoutedBody for PublishResultRecorded {
956    fn msg_type(&self) -> MsgType {
957        MsgType::PublishResultRecorded
958    }
959}
960
961/// Scope type for snapshot requests and responses.
962#[derive(Clone, Debug, Serialize, Deserialize)]
963#[serde(rename_all = "snake_case")]
964pub enum SnapshotScopeType {
965    /// Snapshot of a project.
966    Project,
967    /// Snapshot of a task.
968    Task,
969}
970
971/// A request to take a state snapshot.
972#[derive(Clone, Debug, Serialize, Deserialize)]
973pub struct SnapshotRequest {
974    /// Whether this is a project or task snapshot.
975    pub scope_type: SnapshotScopeType,
976    /// ID of the entity to snapshot.
977    pub scope_id: String,
978}
979
980impl RoutedBody for SnapshotRequest {
981    fn msg_type(&self) -> MsgType {
982        MsgType::SnapshotRequest
983    }
984}
985
986/// A response containing a state snapshot.
987#[derive(Clone, Debug, Serialize, Deserialize)]
988pub struct SnapshotResponse {
989    /// Whether this is a project or task snapshot.
990    pub scope_type: SnapshotScopeType,
991    /// ID of the snapshotted entity.
992    pub scope_id: String,
993    /// The snapshot data.
994    pub snapshot: Value,
995}
996
997impl RoutedBody for SnapshotResponse {
998    fn msg_type(&self) -> MsgType {
999        MsgType::SnapshotResponse
1000    }
1001}
1002
1003/// Scope type for stop orders.
1004#[derive(Clone, Debug, Serialize, Deserialize)]
1005#[serde(rename_all = "snake_case")]
1006pub enum StopScopeType {
1007    /// Stop an entire project.
1008    Project,
1009    /// Stop a task and all its sub-tasks.
1010    TaskTree,
1011}
1012
1013/// An order to stop a project or task tree.
1014#[derive(Clone, Debug, Serialize, Deserialize)]
1015pub struct StopOrder {
1016    /// Unique stop order identifier.
1017    pub stop_id: StopId,
1018    /// Whether this stops a project or a task tree.
1019    pub scope_type: StopScopeType,
1020    /// ID of the entity being stopped.
1021    pub scope_id: String,
1022    /// Machine-readable reason code.
1023    pub reason_code: String,
1024    /// Human-readable reason text.
1025    pub reason_text: String,
1026    /// When the stop order was issued.
1027    pub issued_at: OffsetDateTime,
1028}
1029
1030impl RoutedBody for StopOrder {
1031    fn msg_type(&self) -> MsgType {
1032        MsgType::StopOrder
1033    }
1034}
1035
1036/// State in a stop acknowledgment.
1037#[derive(Clone, Debug, Serialize, Deserialize)]
1038#[serde(rename_all = "snake_case")]
1039pub enum StopAckState {
1040    /// The actor is in the process of stopping.
1041    Stopping,
1042}
1043
1044/// Acknowledgment that a stop order was received and is being processed.
1045#[derive(Clone, Debug, Serialize, Deserialize)]
1046pub struct StopAck {
1047    /// The stop order being acknowledged.
1048    pub stop_id: StopId,
1049    /// The actor sending this acknowledgment.
1050    pub actor_id: ActorId,
1051    /// Current stop state of the acknowledging actor.
1052    pub ack_state: StopAckState,
1053    /// When this acknowledgment was sent.
1054    pub acked_at: OffsetDateTime,
1055}
1056
1057impl RoutedBody for StopAck {
1058    fn msg_type(&self) -> MsgType {
1059        MsgType::StopAck
1060    }
1061}
1062
1063/// Final state in a stop completion message.
1064#[derive(Clone, Debug, Serialize, Deserialize)]
1065#[serde(rename_all = "snake_case")]
1066pub enum StopFinalState {
1067    /// The actor has fully stopped.
1068    Stopped,
1069}
1070
1071/// Confirmation that all work has stopped for a given stop order.
1072#[derive(Clone, Debug, Serialize, Deserialize)]
1073pub struct StopComplete {
1074    /// The stop order that has been completed.
1075    pub stop_id: StopId,
1076    /// The actor confirming completion.
1077    pub actor_id: ActorId,
1078    /// The final state (always `Stopped`).
1079    pub final_state: StopFinalState,
1080    /// When the stop was fully completed.
1081    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}