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