1pub fn payload_type(suffix: &str) -> String {
11 format!("application/vnd.treeship.{}.v1+json", suffix)
12}
13
14pub const TYPE_ACTION: &str = "treeship/action/v1";
15pub const TYPE_APPROVAL: &str = "treeship/approval/v1";
16pub const TYPE_HANDOFF: &str = "treeship/handoff/v1";
17pub const TYPE_ENDORSEMENT: &str = "treeship/endorsement/v1";
18pub const TYPE_RECEIPT: &str = "treeship/receipt/v1";
19pub const TYPE_BUNDLE: &str = "treeship/bundle/v1";
20pub const TYPE_DECISION: &str = "treeship/decision/v1";
21
22mod approval_use;
26pub use approval_use::{
27 ApprovalRevocation, ApprovalUse, CheckpointKind, HubCheckpointVerification,
28 JournalCheckpoint, ReplayCheck, ReplayCheckLevel,
29 TYPE_APPROVAL_REVOCATION, TYPE_APPROVAL_USE, TYPE_JOURNAL_CHECKPOINT,
30 approval_revocation_record_digest, approval_use_record_digest,
31 journal_checkpoint_record_digest, nonce_digest, verify_hub_checkpoint_signature,
32};
33
34pub mod invitation;
40pub mod session_participant;
41pub use invitation::{
42 GrantedCapabilities, InvitationError, InvitationStatement, InviteeRestriction,
43 TYPE_INVITATION, DEFAULT_INVITATION_LIFETIME_SECS, MAX_INVITATION_LIFETIME_SECS,
44};
45pub use session_participant::{
46 ParticipantVerifyError, SessionParticipantStatement, TYPE_SESSION_PARTICIPANT,
47 verify_participant_envelope,
48};
49
50use serde::{Deserialize, Serialize};
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55pub struct SubjectRef {
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub digest: Option<String>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub uri: Option<String>,
63
64 #[serde(rename = "artifactId", skip_serializing_if = "Option::is_none")]
66 pub artifact_id: Option<String>,
67}
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84pub struct ApprovalScope {
85 #[serde(rename = "maxActions", skip_serializing_if = "Option::is_none")]
89 pub max_actions: Option<u32>,
90
91 #[serde(rename = "validUntil", skip_serializing_if = "Option::is_none")]
96 pub valid_until: Option<String>,
97
98 #[serde(rename = "allowedActors", skip_serializing_if = "Vec::is_empty", default)]
101 pub allowed_actors: Vec<String>,
102
103 #[serde(rename = "allowedActions", skip_serializing_if = "Vec::is_empty", default)]
106 pub allowed_actions: Vec<String>,
107
108 #[serde(rename = "allowedSubjects", skip_serializing_if = "Vec::is_empty", default)]
113 pub allowed_subjects: Vec<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub extra: Option<serde_json::Value>,
118}
119
120impl ApprovalScope {
121 pub fn is_unscoped(&self) -> bool {
126 self.max_actions.is_none()
127 && self.valid_until.is_none()
128 && self.allowed_actors.is_empty()
129 && self.allowed_actions.is_empty()
130 && self.allowed_subjects.is_empty()
131 && self.extra.is_none()
132 }
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct ActionStatement {
141 #[serde(rename = "type")]
143 pub type_: String,
144
145 pub timestamp: String,
147
148 pub actor: String,
150
151 pub action: String,
153
154 #[serde(default, skip_serializing_if = "is_empty_subject")]
155 pub subject: SubjectRef,
156
157 #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
159 pub parent_id: Option<String>,
160
161 #[serde(rename = "approvalNonce", skip_serializing_if = "Option::is_none")]
165 pub approval_nonce: Option<String>,
166
167 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
168 pub policy_ref: Option<String>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub meta: Option<serde_json::Value>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct ApprovalStatement {
182 #[serde(rename = "type")]
183 pub type_: String,
184 pub timestamp: String,
185
186 pub approver: String,
188
189 #[serde(default, skip_serializing_if = "is_empty_subject")]
190 pub subject: SubjectRef,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub description: Option<String>,
194
195 #[serde(rename = "expiresAt", skip_serializing_if = "Option::is_none")]
197 pub expires_at: Option<String>,
198
199 pub delegatable: bool,
201
202 pub nonce: String,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub scope: Option<ApprovalScope>,
209
210 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
211 pub policy_ref: Option<String>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub meta: Option<serde_json::Value>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct HandoffStatement {
223 #[serde(rename = "type")]
224 pub type_: String,
225 pub timestamp: String,
226
227 pub from: String,
229 pub to: String,
231
232 pub artifacts: Vec<String>,
234
235 #[serde(rename = "approvalIds", default, skip_serializing_if = "Vec::is_empty")]
237 pub approval_ids: Vec<String>,
238
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
241 pub obligations: Vec<String>,
242
243 pub delegatable: bool,
244
245 #[serde(rename = "taskRef", skip_serializing_if = "Option::is_none")]
246 pub task_ref: Option<String>,
247
248 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
249 pub policy_ref: Option<String>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub meta: Option<serde_json::Value>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct EndorsementStatement {
260 #[serde(rename = "type")]
261 pub type_: String,
262 pub timestamp: String,
263
264 pub endorser: String,
266 pub subject: SubjectRef,
267
268 pub kind: String,
271
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub rationale: Option<String>,
274
275 #[serde(rename = "expiresAt", skip_serializing_if = "Option::is_none")]
276 pub expires_at: Option<String>,
277
278 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
279 pub policy_ref: Option<String>,
280
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub meta: Option<serde_json::Value>,
283}
284
285impl EndorsementStatement {
286 pub fn new(endorser: impl Into<String>, kind: impl Into<String>) -> Self {
287 Self {
288 type_: TYPE_ENDORSEMENT.into(),
289 timestamp: now_rfc3339(),
290 endorser: endorser.into(),
291 subject: SubjectRef::default(),
292 kind: kind.into(),
293 rationale: None,
294 expires_at: None,
295 policy_ref: None,
296 meta: None,
297 }
298 }
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct ReceiptStatement {
306 #[serde(rename = "type")]
307 pub type_: String,
308 pub timestamp: String,
309
310 pub system: String,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub subject: Option<SubjectRef>,
316
317 pub kind: String,
319
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub payload: Option<serde_json::Value>,
322
323 #[serde(rename = "payloadDigest", skip_serializing_if = "Option::is_none")]
324 pub payload_digest: Option<String>,
325
326 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
327 pub policy_ref: Option<String>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
330 pub meta: Option<serde_json::Value>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct ArtifactRef {
336 pub id: String,
337 pub digest: String,
338 #[serde(rename = "type")]
339 pub type_: String,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct BundleStatement {
345 #[serde(rename = "type")]
346 pub type_: String,
347 pub timestamp: String,
348
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub tag: Option<String>,
351
352 #[serde(skip_serializing_if = "Option::is_none")]
353 pub description: Option<String>,
354
355 pub artifacts: Vec<ArtifactRef>,
356
357 #[serde(rename = "policyRef", skip_serializing_if = "Option::is_none")]
358 pub policy_ref: Option<String>,
359
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub meta: Option<serde_json::Value>,
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct DecisionStatement {
370 #[serde(rename = "type")]
372 pub type_: String,
373
374 pub timestamp: String,
376
377 pub actor: String,
379
380 #[serde(rename = "parentId", skip_serializing_if = "Option::is_none")]
382 pub parent_id: Option<String>,
383
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub model: Option<String>,
387
388 #[serde(rename = "modelVersion", skip_serializing_if = "Option::is_none")]
390 pub model_version: Option<String>,
391
392 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub provider: Option<String>,
407
408 #[serde(rename = "tokensIn", skip_serializing_if = "Option::is_none")]
410 pub tokens_in: Option<u64>,
411
412 #[serde(rename = "tokensOut", skip_serializing_if = "Option::is_none")]
414 pub tokens_out: Option<u64>,
415
416 #[serde(rename = "promptDigest", skip_serializing_if = "Option::is_none")]
418 pub prompt_digest: Option<String>,
419
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub summary: Option<String>,
423
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub confidence: Option<f64>,
427
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub alternatives: Option<Vec<String>>,
431
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub meta: Option<serde_json::Value>,
435}
436
437fn is_empty_subject(s: &SubjectRef) -> bool {
439 s.digest.is_none() && s.uri.is_none() && s.artifact_id.is_none()
440}
441
442impl ActionStatement {
445 pub fn new(actor: impl Into<String>, action: impl Into<String>) -> Self {
446 Self {
447 type_: TYPE_ACTION.into(),
448 timestamp: now_rfc3339(),
449 actor: actor.into(),
450 action: action.into(),
451 subject: SubjectRef::default(),
452 parent_id: None,
453 approval_nonce: None,
454 policy_ref: None,
455 meta: None,
456 }
457 }
458}
459
460impl ApprovalStatement {
461 pub fn new(approver: impl Into<String>, nonce: impl Into<String>) -> Self {
462 Self {
463 type_: TYPE_APPROVAL.into(),
464 timestamp: now_rfc3339(),
465 approver: approver.into(),
466 subject: SubjectRef::default(),
467 description: None,
468 expires_at: None,
469 delegatable: false,
470 nonce: nonce.into(),
471 scope: None,
472 policy_ref: None,
473 meta: None,
474 }
475 }
476}
477
478impl HandoffStatement {
479 pub fn new(
480 from: impl Into<String>,
481 to: impl Into<String>,
482 artifacts: Vec<String>,
483 ) -> Self {
484 Self {
485 type_: TYPE_HANDOFF.into(),
486 timestamp: now_rfc3339(),
487 from: from.into(),
488 to: to.into(),
489 artifacts,
490 approval_ids: vec![],
491 obligations: vec![],
492 delegatable: false,
493 task_ref: None,
494 policy_ref: None,
495 meta: None,
496 }
497 }
498}
499
500impl ReceiptStatement {
501 pub fn new(system: impl Into<String>, kind: impl Into<String>) -> Self {
502 Self {
503 type_: TYPE_RECEIPT.into(),
504 timestamp: now_rfc3339(),
505 system: system.into(),
506 subject: None,
507 kind: kind.into(),
508 payload: None,
509 payload_digest: None,
510 policy_ref: None,
511 meta: None,
512 }
513 }
514}
515
516impl DecisionStatement {
517 pub fn new(actor: impl Into<String>) -> Self {
518 Self {
519 type_: TYPE_DECISION.into(),
520 timestamp: now_rfc3339(),
521 actor: actor.into(),
522 parent_id: None,
523 model: None,
524 model_version: None,
525 provider: None,
526 tokens_in: None,
527 tokens_out: None,
528 prompt_digest: None,
529 summary: None,
530 confidence: None,
531 alternatives: None,
532 meta: None,
533 }
534 }
535}
536
537fn now_rfc3339() -> String {
538 use std::time::{SystemTime, UNIX_EPOCH};
541 let secs = SystemTime::now()
542 .duration_since(UNIX_EPOCH)
543 .unwrap_or_default()
544 .as_secs();
545 unix_to_rfc3339(secs)
546}
547
548pub fn unix_to_rfc3339(secs: u64) -> String {
549 let s = secs;
552 let (y, mo, d, h, mi, sec) = seconds_to_ymd_hms(s);
553 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, mo, d, h, mi, sec)
554}
555
556fn seconds_to_ymd_hms(s: u64) -> (u64, u64, u64, u64, u64, u64) {
557 let sec = s % 60;
558 let mins = s / 60;
559 let min = mins % 60;
560 let hrs = mins / 60;
561 let hour = hrs % 24;
562 let days = hrs / 24;
563
564 let (y, m, d) = days_to_ymd(days);
566 (y, m, d, hour, min, sec)
567}
568
569fn days_to_ymd(days: u64) -> (u64, u64, u64) {
570 let mut d = days;
572 let mut year = 1970u64;
573 loop {
574 let dy = if is_leap(year) { 366 } else { 365 };
575 if d < dy { break; }
576 d -= dy;
577 year += 1;
578 }
579 let months = if is_leap(year) {
580 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
581 } else {
582 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
583 };
584 let mut month = 1u64;
585 for dm in months {
586 if d < dm { break; }
587 d -= dm;
588 month += 1;
589 }
590 (year, month, d + 1)
591}
592
593fn is_leap(y: u64) -> bool {
594 (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use crate::attestation::{sign, Ed25519Signer, Verifier};
601
602 #[test]
603 fn payload_type_format() {
604 assert_eq!(
605 payload_type("action"),
606 "application/vnd.treeship.action.v1+json"
607 );
608 assert_eq!(
609 payload_type("approval"),
610 "application/vnd.treeship.approval.v1+json"
611 );
612 }
613
614 #[test]
615 fn action_statement_sign_verify() {
616 let signer = Ed25519Signer::generate("key_test").unwrap();
617 let verifier = Verifier::from_signer(&signer);
618
619 let mut stmt = ActionStatement::new("agent://researcher", "tool.call");
620 stmt.parent_id = Some("art_aabbccdd11223344aabbccdd11223344".into());
621
622 let pt = payload_type("action");
623 let result = sign(&pt, &stmt, &signer).unwrap();
624
625 assert!(result.artifact_id.starts_with("art_"));
626
627 let vr = verifier.verify(&result.envelope).unwrap();
628 assert_eq!(vr.artifact_id, result.artifact_id);
629
630 let decoded: ActionStatement = result.envelope.unmarshal_statement().unwrap();
632 assert_eq!(decoded.actor, "agent://researcher");
633 assert_eq!(decoded.action, "tool.call");
634 assert_eq!(decoded.type_, TYPE_ACTION);
635 }
636
637 #[test]
638 fn approval_statement_with_nonce() {
639 let signer = Ed25519Signer::generate("key_human").unwrap();
640
641 let mut approval = ApprovalStatement::new("human://alice", "nonce_abc123");
642 approval.description = Some("approve laptop purchase < $1500".into());
643 approval.scope = Some(ApprovalScope {
644 max_actions: Some(1),
645 allowed_actions: vec!["stripe.payment_intent.create".into()],
646 ..Default::default()
647 });
648
649 let pt = payload_type("approval");
650 let result = sign(&pt, &approval, &signer).unwrap();
651 assert!(result.artifact_id.starts_with("art_"));
652
653 let decoded: ApprovalStatement = result.envelope.unmarshal_statement().unwrap();
654 assert_eq!(decoded.nonce, "nonce_abc123");
655 assert_eq!(decoded.scope.unwrap().max_actions, Some(1));
656 }
657
658 #[test]
659 fn approval_scope_full_grant_roundtrips() {
660 let signer = Ed25519Signer::generate("key_piyush").unwrap();
664
665 let mut approval = ApprovalStatement::new("human://piyush", "nonce_deadbeef");
666 approval.description = Some("Deploy production after final review".into());
667 approval.scope = Some(ApprovalScope {
668 max_actions: Some(1),
669 valid_until: None,
670 allowed_actors: vec!["agent://deployer".into()],
671 allowed_actions: vec!["deploy.production".into()],
672 allowed_subjects: vec!["env://production".into()],
673 extra: None,
674 });
675
676 let pt = payload_type("approval");
677 let result = sign(&pt, &approval, &signer).unwrap();
678 let decoded: ApprovalStatement = result.envelope.unmarshal_statement().unwrap();
679 let scope = decoded.scope.expect("scope must round-trip");
680
681 assert_eq!(scope.allowed_actors, vec!["agent://deployer".to_string()]);
682 assert_eq!(scope.allowed_actions, vec!["deploy.production".to_string()]);
683 assert_eq!(scope.allowed_subjects, vec!["env://production".to_string()]);
684 assert_eq!(scope.max_actions, Some(1));
685 }
686
687 #[test]
688 fn approval_scope_is_unscoped_predicate() {
689 assert!(ApprovalScope::default().is_unscoped());
691
692 assert!(!ApprovalScope { max_actions: Some(1), ..Default::default() }.is_unscoped());
694 assert!(!ApprovalScope { valid_until: Some("2030-01-01T00:00:00Z".into()), ..Default::default() }.is_unscoped());
695 assert!(!ApprovalScope { allowed_actors: vec!["agent://x".into()], ..Default::default() }.is_unscoped());
696 assert!(!ApprovalScope { allowed_actions: vec!["doit".into()], ..Default::default() }.is_unscoped());
697 assert!(!ApprovalScope { allowed_subjects: vec!["env://prod".into()], ..Default::default() }.is_unscoped());
698 }
699
700 #[test]
701 fn approval_scope_legacy_payloads_decode_with_empty_new_fields() {
702 let legacy = serde_json::json!({
706 "maxActions": 1,
707 "allowedActions": ["stripe.payment_intent.create"]
708 });
709 let scope: ApprovalScope = serde_json::from_value(legacy).unwrap();
710 assert_eq!(scope.max_actions, Some(1));
711 assert_eq!(scope.allowed_actions, vec!["stripe.payment_intent.create".to_string()]);
712 assert!(scope.allowed_actors.is_empty());
714 assert!(scope.allowed_subjects.is_empty());
715 assert!(!scope.is_unscoped()); }
717
718 #[test]
719 fn handoff_statement() {
720 let signer = Ed25519Signer::generate("key_agent").unwrap();
721
722 let handoff = HandoffStatement::new(
723 "agent://researcher",
724 "agent://checkout",
725 vec!["art_aabbccdd11223344aabbccdd11223344".into()],
726 );
727
728 let pt = payload_type("handoff");
729 let result = sign(&pt, &handoff, &signer).unwrap();
730 let decoded: HandoffStatement = result.envelope.unmarshal_statement().unwrap();
731
732 assert_eq!(decoded.from, "agent://researcher");
733 assert_eq!(decoded.to, "agent://checkout");
734 assert_eq!(decoded.artifacts.len(), 1);
735 }
736
737 #[test]
738 fn receipt_statement() {
739 let signer = Ed25519Signer::generate("key_system").unwrap();
740
741 let mut receipt = ReceiptStatement::new("system://stripe-webhook", "confirmation");
742 receipt.payload = Some(serde_json::json!({
743 "eventId": "evt_abc123",
744 "status": "succeeded"
745 }));
746
747 let pt = payload_type("receipt");
748 let result = sign(&pt, &receipt, &signer).unwrap();
749 let decoded: ReceiptStatement = result.envelope.unmarshal_statement().unwrap();
750
751 assert_eq!(decoded.system, "system://stripe-webhook");
752 assert_eq!(decoded.kind, "confirmation");
753 }
754
755 #[test]
756 fn nonce_binding_survives_serialization() {
757 let signer = Ed25519Signer::generate("key_test").unwrap();
758
759 let approval = ApprovalStatement::new("human://alice", "secure_nonce_xyz");
762 let pt = payload_type("approval");
763 let signed = sign(&pt, &approval, &signer).unwrap();
764
765 let decoded: ApprovalStatement = signed.envelope.unmarshal_statement().unwrap();
766 assert_eq!(decoded.nonce, "secure_nonce_xyz", "nonce must survive serialization");
767 }
768
769 #[test]
770 fn decision_statement_sign_verify() {
771 let signer = Ed25519Signer::generate("key_test").unwrap();
772 let verifier = Verifier::from_signer(&signer);
773
774 let mut stmt = DecisionStatement::new("agent://analyst");
775 stmt.model = Some("claude-opus-4".into());
776 stmt.tokens_in = Some(8432);
777 stmt.tokens_out = Some(1247);
778 stmt.summary = Some("Contract looks standard.".into());
779 stmt.confidence = Some(0.91);
780
781 let pt = payload_type("decision");
782 let result = sign(&pt, &stmt, &signer).unwrap();
783
784 assert!(result.artifact_id.starts_with("art_"));
785
786 let vr = verifier.verify(&result.envelope).unwrap();
787 assert_eq!(vr.artifact_id, result.artifact_id);
788
789 let decoded: DecisionStatement = result.envelope.unmarshal_statement().unwrap();
791 assert_eq!(decoded.actor, "agent://analyst");
792 assert_eq!(decoded.model, Some("claude-opus-4".into()));
793 assert_eq!(decoded.tokens_in, Some(8432));
794 assert_eq!(decoded.tokens_out, Some(1247));
795 assert_eq!(decoded.summary, Some("Contract looks standard.".into()));
796 assert_eq!(decoded.confidence, Some(0.91));
797 assert_eq!(decoded.type_, TYPE_DECISION);
798 }
799
800 #[test]
801 fn decision_statement_provider_roundtrips() {
802 let signer = Ed25519Signer::generate("key_test").unwrap();
806 let verifier = Verifier::from_signer(&signer);
807
808 let mut stmt = DecisionStatement::new("agent://researcher");
809 stmt.model = Some("kimi-k2".into());
810 stmt.provider = Some("moonshot".into());
811
812 let pt = payload_type("decision");
813 let result = sign(&pt, &stmt, &signer).unwrap();
814 verifier.verify(&result.envelope).unwrap();
815
816 let decoded: DecisionStatement = result.envelope.unmarshal_statement().unwrap();
817 assert_eq!(decoded.model, Some("kimi-k2".into()));
818 assert_eq!(decoded.provider, Some("moonshot".into()));
819 }
820
821 #[test]
822 fn decision_statement_legacy_payload_without_provider_decodes() {
823 let raw = serde_json::json!({
829 "type": TYPE_DECISION,
830 "timestamp": "2026-04-30T12:00:00Z",
831 "actor": "agent://legacy",
832 "model": "claude-opus-4",
833 });
834 let parsed: DecisionStatement = serde_json::from_value(raw).unwrap();
835 assert_eq!(parsed.model, Some("claude-opus-4".into()));
836 assert_eq!(parsed.provider, None);
837 }
838
839 #[test]
840 fn different_statement_types_different_ids() {
841 let signer = Ed25519Signer::generate("key_test").unwrap();
844
845 let action = ActionStatement::new("agent://test", "do.thing");
846 let approval = ApprovalStatement::new("human://test", "nonce_123");
847
848 let r_action = sign(&payload_type("action"), &action, &signer).unwrap();
849 let r_approval = sign(&payload_type("approval"), &approval, &signer).unwrap();
850
851 assert_ne!(r_action.artifact_id, r_approval.artifact_id);
852 }
853
854 #[test]
855 fn timestamp_format() {
856 let ts = unix_to_rfc3339(0);
857 assert_eq!(ts, "1970-01-01T00:00:00Z");
858
859 let ts2 = unix_to_rfc3339(1_000_000_000);
860 assert_eq!(ts2, "2001-09-09T01:46:40Z");
861 }
862}