1use std::collections::BTreeMap;
5use std::fmt;
6
7use serde::{Deserialize, Serialize};
8
9use crate::changeid::ChangeId;
10use crate::objects::GitId;
11
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(tag = "kind", rename_all = "snake_case")]
14pub enum Actor {
15 Human { name: String },
16 Agent { name: String, session: String },
17 Daemon,
18}
19
20impl Actor {
21 pub fn label(&self) -> String {
22 match self {
23 Actor::Human { name } => format!("human:{name}"),
24 Actor::Agent { name, session } => format!("agent:{name}/{session}"),
25 Actor::Daemon => "daemon".into(),
26 }
27 }
28
29 pub fn session(&self) -> Option<&str> {
30 match self {
31 Actor::Agent { session, .. } => Some(session),
32 _ => None,
33 }
34 }
35}
36
37impl fmt::Display for Actor {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 f.write_str(&self.label())
40 }
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
46pub enum OpKind {
47 #[serde(rename = "repo.init")]
48 RepoInit,
49 #[serde(rename = "human.enroll")]
50 HumanEnroll,
51 #[serde(rename = "human.rotate")]
52 HumanRotate,
53 #[serde(rename = "human.revoke")]
54 HumanRevoke,
55 #[serde(rename = "human.recovery_key")]
56 HumanRecoveryKey,
57 #[serde(rename = "human.recover")]
58 HumanRecover,
59 #[serde(rename = "daemon.rotate")]
60 DaemonRotate,
61 #[serde(rename = "daemon.recover")]
62 DaemonRecover,
63 #[serde(rename = "policy.update")]
64 PolicyUpdate,
65 #[serde(rename = "session.start")]
66 SessionStart,
67 #[serde(rename = "session.bind")]
68 SessionBind,
69 #[serde(rename = "session.isolate")]
70 SessionIsolate,
71 #[serde(rename = "session.end")]
72 SessionEnd,
73 #[serde(rename = "cap.attenuate")]
74 CapAttenuate,
75 #[serde(rename = "cap.revoke")]
76 CapRevoke,
77 #[serde(rename = "lease.acquire")]
78 LeaseAcquire,
79 #[serde(rename = "lease.release")]
80 LeaseRelease,
81 #[serde(rename = "lease.renew")]
82 LeaseRenew,
83 #[serde(rename = "lease.interest")]
84 LeaseInterest,
85 #[serde(rename = "change.propose")]
86 ChangePropose,
87 #[serde(rename = "change.withdraw")]
88 ChangeWithdraw,
89 #[serde(rename = "change.integrate")]
90 ChangeIntegrate,
91 #[serde(rename = "change.integrate_batch")]
92 ChangeIntegrateBatch,
93 #[serde(rename = "approve")]
94 Approve,
95 #[serde(rename = "reject")]
96 Reject,
97 #[serde(rename = "observe")]
98 Observe,
99 #[serde(rename = "taint")]
100 Taint,
101 #[serde(rename = "import.external")]
102 ImportExternal,
103 #[serde(rename = "import.unmediated")]
104 ImportUnmediated,
105 #[serde(rename = "bridge.push")]
106 BridgePush,
107 #[serde(rename = "freeze")]
108 Freeze,
109 #[serde(rename = "unfreeze")]
110 Unfreeze,
111 #[serde(rename = "negotiate")]
112 Negotiate,
113 #[serde(rename = "redact")]
114 Redact,
115}
116
117impl OpKind {
118 pub fn as_str(self) -> &'static str {
119 match self {
120 OpKind::RepoInit => "repo.init",
121 OpKind::HumanEnroll => "human.enroll",
122 OpKind::HumanRotate => "human.rotate",
123 OpKind::HumanRevoke => "human.revoke",
124 OpKind::HumanRecoveryKey => "human.recovery_key",
125 OpKind::HumanRecover => "human.recover",
126 OpKind::DaemonRotate => "daemon.rotate",
127 OpKind::DaemonRecover => "daemon.recover",
128 OpKind::PolicyUpdate => "policy.update",
129 OpKind::SessionStart => "session.start",
130 OpKind::SessionBind => "session.bind",
131 OpKind::SessionIsolate => "session.isolate",
132 OpKind::SessionEnd => "session.end",
133 OpKind::CapAttenuate => "cap.attenuate",
134 OpKind::CapRevoke => "cap.revoke",
135 OpKind::LeaseAcquire => "lease.acquire",
136 OpKind::LeaseRelease => "lease.release",
137 OpKind::LeaseRenew => "lease.renew",
138 OpKind::LeaseInterest => "lease.interest",
139 OpKind::ChangePropose => "change.propose",
140 OpKind::ChangeWithdraw => "change.withdraw",
141 OpKind::ChangeIntegrate => "change.integrate",
142 OpKind::ChangeIntegrateBatch => "change.integrate_batch",
143 OpKind::Approve => "approve",
144 OpKind::Reject => "reject",
145 OpKind::Observe => "observe",
146 OpKind::Taint => "taint",
147 OpKind::ImportExternal => "import.external",
148 OpKind::ImportUnmediated => "import.unmediated",
149 OpKind::BridgePush => "bridge.push",
150 OpKind::Freeze => "freeze",
151 OpKind::Unfreeze => "unfreeze",
152 OpKind::Negotiate => "negotiate",
153 OpKind::Redact => "redact",
154 }
155 }
156
157 pub fn agent_grantable(self) -> bool {
162 matches!(
163 self,
164 OpKind::ChangePropose
165 | OpKind::ChangeWithdraw
166 | OpKind::LeaseAcquire
167 | OpKind::LeaseRelease
168 | OpKind::LeaseRenew
169 | OpKind::LeaseInterest
170 | OpKind::Negotiate
171 | OpKind::CapAttenuate
172 )
173 }
174}
175
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
178pub struct Scope {
179 pub paths: Vec<String>,
180 pub symbols: Option<Vec<String>>,
182 pub ops: Vec<OpKind>,
183 pub max_files: Option<u32>,
184 pub max_lines: Option<u32>,
185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
190 pub scratch: Vec<String>,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
194#[serde(rename_all = "snake_case")]
195pub enum ObservationLevel {
196 Unobserved,
198 Hooks,
200 HooksSandbox,
202 Isolated,
204}
205
206#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
207pub struct BindChecks {
208 pub harness: String,
209 pub harness_session: String,
210 pub managed_hooks: bool,
212 pub locked_sandbox: bool,
214 pub sudo_without_password: bool,
216 pub config_digest: String,
218 pub hook_binary_protected: bool,
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum LeaseKind {
225 Edit,
227 Insert,
229 Rename,
231 Shared,
235}
236
237impl LeaseKind {
238 pub const ALL: [LeaseKind; 4] = [Self::Edit, Self::Insert, Self::Rename, Self::Shared];
239
240 pub fn as_str(self) -> &'static str {
242 match self {
243 Self::Edit => "edit",
244 Self::Insert => "insert",
245 Self::Rename => "rename",
246 Self::Shared => "shared",
247 }
248 }
249
250 pub fn from_name(name: &str) -> Option<Self> {
251 Self::ALL.into_iter().find(|k| k.as_str() == name)
252 }
253}
254
255impl LeaseItem {
256 pub fn new(symbol: impl Into<String>, kind: LeaseKind) -> Self {
258 Self {
259 symbol: symbol.into(),
260 kind,
261 names: Vec::new(),
262 }
263 }
264
265 pub fn is_shared(&self) -> bool {
266 self.kind == LeaseKind::Shared
267 }
268}
269
270#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
271pub struct LeaseItem {
272 pub symbol: String,
274 pub kind: LeaseKind,
275 #[serde(default, skip_serializing_if = "Vec::is_empty")]
281 pub names: Vec<String>,
282}
283
284#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
285pub struct TestDelta {
286 pub added: u32,
287 pub modified: u32,
288 pub deleted: u32,
289 pub skipped: u32,
291}
292
293#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
296pub struct Claim {
297 pub intent: String,
299 pub edits: Vec<String>,
300 pub adds: Vec<String>,
301 pub deletes: Vec<String>,
302 pub deleted_files: Vec<String>,
303 pub signature_changes: Vec<String>,
304 pub tests: TestDelta,
305 pub revert_of: Option<ChangeId>,
306}
307
308pub const DEF: &str = "<def>";
312
313pub fn names_are_headings(path: &str) -> bool {
316 let ext = path.rsplit_once('.').map(|(_, ext)| ext);
317 matches!(
318 ext,
319 Some("md" | "markdown" | "toml" | "json" | "yml" | "yaml")
320 )
321}
322
323pub const ENGINE_DEFS: u32 = 3;
326
327#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
329pub struct Facts {
330 pub edits: Vec<String>,
331 pub adds: Vec<String>,
332 pub deletes: Vec<String>,
333 pub added_files: Vec<String>,
334 pub deleted_files: Vec<String>,
335 pub modified_files: Vec<String>,
336 pub signature_changes: Vec<String>,
337 pub import_only_files: Vec<String>,
339 pub tests: TestDelta,
340 pub lines_added: u32,
341 pub lines_removed: u32,
342 #[serde(default, skip_serializing_if = "Vec::is_empty")]
345 pub renames: Vec<[String; 2]>,
346 #[serde(default, skip_serializing_if = "Vec::is_empty")]
349 pub unparsed_files: Vec<String>,
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
354 pub unread_files: Vec<String>,
355 #[serde(default, skip_serializing_if = "Vec::is_empty")]
359 pub affected: Vec<String>,
360 #[serde(default, skip_serializing_if = "is_zero")]
362 pub affected_more: u32,
363}
364
365fn is_zero(n: &u32) -> bool {
366 *n == 0
367}
368
369impl Facts {
370 pub fn reaches_others(&self) -> bool {
373 !self.signature_changes.is_empty() || !self.affected.is_empty() || self.affected_more > 0
374 }
375}
376
377#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
378#[serde(rename_all = "snake_case")]
379pub enum CheckKind {
380 Syntax,
381 Scope,
382 ClaimDiff,
383 Merge,
384 Build,
385 Tests,
386 TestIntegrity,
387 Extended,
389 Named,
391}
392
393#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "snake_case")]
395pub enum CheckResult {
396 Pass,
397 Fail,
398 Error,
399 Skipped,
400}
401
402#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
405pub struct Attestation {
406 pub check: CheckKind,
407 pub result: CheckResult,
408 pub subject_tree: GitId,
409 pub details: BTreeMap<String, String>,
410 pub env: String,
412 pub log: Option<GitId>,
413 pub producer: String,
414}
415
416#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(rename_all = "snake_case")]
418pub enum Phase {
419 Pre,
420 Post,
421}
422
423#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(tag = "type", rename_all = "snake_case")]
425pub enum ObsKind {
426 Read {
427 path: String,
428 },
429 Write {
430 path: String,
431 },
432 Shell {
435 program: String,
436 digest: String,
437 },
438 Fetch {
439 host: String,
440 },
441 Mcp {
442 tool: String,
443 },
444 Other {
445 tool: String,
446 },
447 Heartbeat,
448 Unproposed {
453 paths: Vec<String>,
454 total: u64,
455 },
456 Unhooked {
460 tool: String,
461 },
462}
463
464pub const MAX_UNPROPOSED_PATHS: usize = 32;
466
467#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
468pub struct Observation {
469 pub phase: Phase,
470 pub kind: ObsKind,
471 pub external: bool,
473}
474
475#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(tag = "type", rename_all = "snake_case")]
477pub enum NegotiationMsg {
478 Claim {
480 symbols: Vec<String>,
481 reason: String,
482 },
483 ProposeSplit {
485 mine: Vec<String>,
486 yours: Vec<String>,
487 },
488 Yield {
492 set: String,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
494 to: Option<String>,
495 },
496 Counter {
497 note: String,
498 },
499 Accept,
500 Decline {
501 note: String,
502 },
503}
504
505#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
506pub struct HumanKey {
507 pub alg: String,
510 pub public: String,
511}
512
513#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
517pub struct Recovery {
518 pub human: String,
519 pub key: HumanKey,
520}
521
522#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
524pub struct Continuation {
525 pub session: String,
526 pub widen: bool,
528}
529
530#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
531#[serde(tag = "op")]
532pub enum Op {
533 #[serde(rename = "repo.init")]
534 RepoInit {
535 repo: String,
536 daemon_key: String,
537 human: String,
538 human_key: HumanKey,
539 policy: GitId,
540 trunk: GitId,
541 },
542 #[serde(rename = "human.enroll")]
543 HumanEnroll { human: String, key: HumanKey },
544 #[serde(rename = "human.rotate")]
546 HumanRotate { human: String, key: HumanKey },
547 #[serde(rename = "human.revoke")]
549 HumanRevoke { human: String, reason: String },
550 #[serde(rename = "human.recovery_key")]
553 HumanRecoveryKey { human: String, key: HumanKey },
554 #[serde(rename = "human.recover")]
557 HumanRecover { human: String, key: HumanKey },
558 #[serde(rename = "daemon.rotate")]
561 DaemonRotate { key: String },
562 #[serde(rename = "daemon.recover")]
568 DaemonRecover {
569 key: String,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
571 recover: Option<Recovery>,
572 },
573 #[serde(rename = "policy.update")]
574 PolicyUpdate { policy: GitId },
575 #[serde(rename = "session.start")]
576 SessionStart {
577 session: String,
578 agent: String,
579 model: Option<String>,
580 task: String,
581 scope: Scope,
582 expires_ms: i64,
583 #[serde(default, skip_serializing_if = "Option::is_none")]
585 continues: Option<Continuation>,
586 #[serde(default, skip_serializing_if = "Option::is_none")]
589 harness: Option<String>,
590 },
591 #[serde(rename = "session.bind")]
592 SessionBind {
593 session: String,
594 checks: BindChecks,
595 level: ObservationLevel,
596 },
597 #[serde(rename = "session.isolate")]
602 SessionIsolate {
603 session: String,
604 sandbox: String,
606 profile: String,
608 },
609 #[serde(rename = "session.end")]
610 SessionEnd { session: String, reason: String },
611 #[serde(rename = "cap.attenuate")]
612 CapAttenuate {
613 parent: String,
614 child: String,
615 agent: String,
616 scope: Scope,
617 expires_ms: i64,
618 #[serde(default, skip_serializing_if = "Vec::is_empty")]
621 leases: Vec<String>,
622 },
623 #[serde(rename = "cap.revoke")]
624 CapRevoke { session: String, reason: String },
625 #[serde(rename = "lease.acquire")]
626 LeaseAcquire {
627 set: String,
628 items: Vec<LeaseItem>,
629 intent: String,
630 ttl_ms: i64,
631 },
632 #[serde(rename = "lease.release")]
633 LeaseRelease { set: String },
634 #[serde(rename = "lease.renew")]
635 LeaseRenew { set: String, ttl_ms: i64 },
636 #[serde(rename = "lease.interest")]
640 LeaseInterest {
641 items: Vec<LeaseItem>,
642 intent: String,
643 },
644 #[serde(rename = "change.propose")]
645 ChangePropose {
646 change: ChangeId,
647 commit: GitId,
648 base: GitId,
649 claim: Claim,
650 },
651 #[serde(rename = "change.withdraw")]
652 ChangeWithdraw { change: ChangeId },
653 #[serde(rename = "change.integrate")]
654 ChangeIntegrate {
655 change: ChangeId,
656 trunk_before: GitId,
657 merged: Option<GitId>,
659 conflicts: Vec<String>,
660 },
661 #[serde(rename = "change.integrate_batch")]
666 ChangeIntegrateBatch {
667 trunk_before: GitId,
668 merged: Vec<(ChangeId, GitId)>,
670 },
671 #[serde(rename = "approve")]
675 Approve {
676 target_seq: u64,
677 note: String,
678 #[serde(default, skip_serializing_if = "Vec::is_empty")]
679 checks: Vec<String>,
680 },
681 #[serde(rename = "reject")]
685 Reject { target_seq: u64, reason: String },
686 #[serde(rename = "observe")]
687 Observe {
688 session: String,
689 events: Vec<Observation>,
690 },
691 #[serde(rename = "taint")]
692 Taint { session: String, reason: String },
693 #[serde(rename = "import.external")]
694 ImportExternal {
695 remote: String,
696 commit: GitId,
697 trunk_before: GitId,
698 },
699 #[serde(rename = "import.unmediated")]
700 ImportUnmediated {
701 refname: String,
702 found: Option<GitId>,
703 expected: Option<GitId>,
704 },
705 #[serde(rename = "bridge.push")]
706 BridgePush {
707 remote: String,
708 oplog_seq: u64,
709 oplog_hash: String,
710 },
711 #[serde(rename = "freeze")]
712 Freeze { reason: String },
713 #[serde(rename = "unfreeze")]
714 Unfreeze { reason: String },
715 #[serde(rename = "negotiate")]
716 Negotiate {
717 thread: String,
718 to_session: String,
719 message: NegotiationMsg,
720 },
721 #[serde(rename = "redact")]
722 Redact { blob: GitId, reason: String },
723}
724
725impl Op {
726 pub fn kind(&self) -> OpKind {
727 match self {
728 Op::RepoInit { .. } => OpKind::RepoInit,
729 Op::HumanEnroll { .. } => OpKind::HumanEnroll,
730 Op::HumanRotate { .. } => OpKind::HumanRotate,
731 Op::HumanRevoke { .. } => OpKind::HumanRevoke,
732 Op::HumanRecoveryKey { .. } => OpKind::HumanRecoveryKey,
733 Op::HumanRecover { .. } => OpKind::HumanRecover,
734 Op::DaemonRotate { .. } => OpKind::DaemonRotate,
735 Op::DaemonRecover { .. } => OpKind::DaemonRecover,
736 Op::PolicyUpdate { .. } => OpKind::PolicyUpdate,
737 Op::SessionStart { .. } => OpKind::SessionStart,
738 Op::SessionBind { .. } => OpKind::SessionBind,
739 Op::SessionIsolate { .. } => OpKind::SessionIsolate,
740 Op::SessionEnd { .. } => OpKind::SessionEnd,
741 Op::CapAttenuate { .. } => OpKind::CapAttenuate,
742 Op::CapRevoke { .. } => OpKind::CapRevoke,
743 Op::LeaseAcquire { .. } => OpKind::LeaseAcquire,
744 Op::LeaseRelease { .. } => OpKind::LeaseRelease,
745 Op::LeaseRenew { .. } => OpKind::LeaseRenew,
746 Op::LeaseInterest { .. } => OpKind::LeaseInterest,
747 Op::ChangePropose { .. } => OpKind::ChangePropose,
748 Op::ChangeWithdraw { .. } => OpKind::ChangeWithdraw,
749 Op::ChangeIntegrate { .. } => OpKind::ChangeIntegrate,
750 Op::ChangeIntegrateBatch { .. } => OpKind::ChangeIntegrateBatch,
751 Op::Approve { .. } => OpKind::Approve,
752 Op::Reject { .. } => OpKind::Reject,
753 Op::Observe { .. } => OpKind::Observe,
754 Op::Taint { .. } => OpKind::Taint,
755 Op::ImportExternal { .. } => OpKind::ImportExternal,
756 Op::ImportUnmediated { .. } => OpKind::ImportUnmediated,
757 Op::BridgePush { .. } => OpKind::BridgePush,
758 Op::Freeze { .. } => OpKind::Freeze,
759 Op::Unfreeze { .. } => OpKind::Unfreeze,
760 Op::Negotiate { .. } => OpKind::Negotiate,
761 Op::Redact { .. } => OpKind::Redact,
762 }
763 }
764
765 pub fn signed_by_recovery_key(&self) -> bool {
768 matches!(
769 self,
770 Op::HumanRecover { .. }
771 | Op::DaemonRecover {
772 recover: Some(_),
773 ..
774 }
775 )
776 }
777
778 pub fn daemon_key_switch(&self) -> Option<&str> {
780 match self {
781 Op::DaemonRotate { key } | Op::DaemonRecover { key, .. } => Some(key),
782 _ => None,
783 }
784 }
785}
786
787#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(rename_all = "snake_case")]
789pub enum Outcome {
790 Allow,
791 Deny,
792 Escalate,
793}
794
795#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
796pub struct Decision {
797 pub outcome: Outcome,
798 pub reasons: Vec<String>,
800}
801
802impl Decision {
803 pub fn allow() -> Self {
804 Self {
805 outcome: Outcome::Allow,
806 reasons: vec![],
807 }
808 }
809
810 pub fn deny(reason: impl Into<String>) -> Self {
811 Self {
812 outcome: Outcome::Deny,
813 reasons: vec![reason.into()],
814 }
815 }
816
817 pub fn escalate(reason: impl Into<String>) -> Self {
818 Self {
819 outcome: Outcome::Escalate,
820 reasons: vec![reason.into()],
821 }
822 }
823
824 pub fn is_allow(&self) -> bool {
825 self.outcome == Outcome::Allow
826 }
827}
828
829#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
832pub struct HumanSignature {
833 pub human: String,
834 pub nonce: String,
835 pub issued_ms: i64,
836 pub sig: String,
837}
838
839#[derive(Serialize)]
840pub struct HumanIntent<'a> {
841 pub purpose: &'static str,
842 pub repo: &'a str,
843 pub op: &'a Op,
844 pub nonce: &'a str,
845 pub issued_ms: i64,
846}
847
848impl<'a> HumanIntent<'a> {
849 pub fn new(repo: &'a str, op: &'a Op, nonce: &'a str, issued_ms: i64) -> Self {
850 Self {
851 purpose: "lonca-human-intent-v1",
852 repo,
853 op,
854 nonce,
855 issued_ms,
856 }
857 }
858}
859
860#[derive(Serialize)]
864pub struct LoginIntent<'a> {
865 pub purpose: &'static str,
866 pub repo: Option<&'a str>,
869 pub human: &'a str,
870 pub ttl_ms: i64,
871 pub nonce: &'a str,
872 pub issued_ms: i64,
873}
874
875impl<'a> LoginIntent<'a> {
876 pub fn new(
877 repo: Option<&'a str>,
878 human: &'a str,
879 ttl_ms: i64,
880 nonce: &'a str,
881 issued_ms: i64,
882 ) -> Self {
883 Self {
884 purpose: "lonca-human-login-v1",
885 repo,
886 human,
887 ttl_ms,
888 nonce,
889 issued_ms,
890 }
891 }
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897
898 #[test]
900 fn lease_kinds_are_named_as_they_are_written() {
901 for kind in LeaseKind::ALL {
902 let written = serde_json::to_value(kind).unwrap();
903 assert_eq!(written, kind.as_str());
904 assert_eq!(LeaseKind::from_name(kind.as_str()), Some(kind));
905 }
906 assert_eq!(LeaseKind::from_name("Shared"), None);
907 assert_eq!(LeaseKind::from_name(""), None);
908 }
909
910 #[test]
911 fn a_change_reaches_others_by_any_one_of_three() {
912 assert!(!Facts::default().reaches_others());
913 let signature = Facts {
914 signature_changes: vec!["a.rs#f".into()],
915 ..Facts::default()
916 };
917 let mentioned = Facts {
918 affected: vec!["a.rs#g".into()],
919 ..Facts::default()
920 };
921 let cut_off = Facts {
922 affected_more: 1,
923 ..Facts::default()
924 };
925 assert!(signature.reaches_others());
926 assert!(mentioned.reaches_others());
927 assert!(cut_off.reaches_others());
928 }
929
930 #[test]
932 fn what_was_cut_from_affected_is_written_out() {
933 let none = serde_json::to_string(&Facts::default()).unwrap();
934 assert!(!none.contains("affected"), "{none}");
935 let cut_off = Facts {
936 affected_more: 3,
937 ..Facts::default()
938 };
939 let text = serde_json::to_string(&cut_off).unwrap();
940 assert!(text.contains("\"affected_more\":3"), "{text}");
941 }
942
943 #[test]
944 fn unknown_op_kind_is_rejected() {
945 assert!(serde_json::from_str::<OpKind>("\"change.propose\"").is_ok());
946 assert!(serde_json::from_str::<OpKind>("\"admin.everything\"").is_err());
947 }
948
949 #[test]
950 fn op_kind_names_match_serde() {
951 for k in [
952 OpKind::RepoInit,
953 OpKind::ChangePropose,
954 OpKind::LeaseAcquire,
955 OpKind::Redact,
956 OpKind::SessionBind,
957 OpKind::SessionIsolate,
958 OpKind::HumanRotate,
959 OpKind::HumanRevoke,
960 OpKind::HumanRecoveryKey,
961 OpKind::HumanRecover,
962 OpKind::DaemonRotate,
963 OpKind::DaemonRecover,
964 ] {
965 assert_eq!(
966 serde_json::to_string(&k).unwrap(),
967 format!("\"{}\"", k.as_str())
968 );
969 }
970 }
971
972 #[test]
973 fn reserved_ops_are_not_grantable() {
974 for k in [
975 OpKind::PolicyUpdate,
976 OpKind::Approve,
977 OpKind::CapRevoke,
978 OpKind::Redact,
979 OpKind::HumanEnroll,
980 OpKind::HumanRotate,
981 OpKind::HumanRevoke,
982 OpKind::HumanRecoveryKey,
983 OpKind::HumanRecover,
984 OpKind::DaemonRotate,
985 OpKind::DaemonRecover,
986 OpKind::Freeze,
987 OpKind::ImportExternal,
988 ] {
989 assert!(!k.agent_grantable(), "{k:?}");
990 }
991 }
992}