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)]
515pub struct Continuation {
516 pub session: String,
517 pub widen: bool,
519}
520
521#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
522#[serde(tag = "op")]
523pub enum Op {
524 #[serde(rename = "repo.init")]
525 RepoInit {
526 repo: String,
527 daemon_key: String,
528 human: String,
529 human_key: HumanKey,
530 policy: GitId,
531 trunk: GitId,
532 },
533 #[serde(rename = "human.enroll")]
534 HumanEnroll { human: String, key: HumanKey },
535 #[serde(rename = "human.rotate")]
537 HumanRotate { human: String, key: HumanKey },
538 #[serde(rename = "human.revoke")]
540 HumanRevoke { human: String, reason: String },
541 #[serde(rename = "human.recovery_key")]
544 HumanRecoveryKey { human: String, key: HumanKey },
545 #[serde(rename = "human.recover")]
548 HumanRecover { human: String, key: HumanKey },
549 #[serde(rename = "daemon.rotate")]
552 DaemonRotate { key: String },
553 #[serde(rename = "daemon.recover")]
557 DaemonRecover { key: String },
558 #[serde(rename = "policy.update")]
559 PolicyUpdate { policy: GitId },
560 #[serde(rename = "session.start")]
561 SessionStart {
562 session: String,
563 agent: String,
564 model: Option<String>,
565 task: String,
566 scope: Scope,
567 expires_ms: i64,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
570 continues: Option<Continuation>,
571 #[serde(default, skip_serializing_if = "Option::is_none")]
574 harness: Option<String>,
575 },
576 #[serde(rename = "session.bind")]
577 SessionBind {
578 session: String,
579 checks: BindChecks,
580 level: ObservationLevel,
581 },
582 #[serde(rename = "session.isolate")]
587 SessionIsolate {
588 session: String,
589 sandbox: String,
591 profile: String,
593 },
594 #[serde(rename = "session.end")]
595 SessionEnd { session: String, reason: String },
596 #[serde(rename = "cap.attenuate")]
597 CapAttenuate {
598 parent: String,
599 child: String,
600 agent: String,
601 scope: Scope,
602 expires_ms: i64,
603 #[serde(default, skip_serializing_if = "Vec::is_empty")]
606 leases: Vec<String>,
607 },
608 #[serde(rename = "cap.revoke")]
609 CapRevoke { session: String, reason: String },
610 #[serde(rename = "lease.acquire")]
611 LeaseAcquire {
612 set: String,
613 items: Vec<LeaseItem>,
614 intent: String,
615 ttl_ms: i64,
616 },
617 #[serde(rename = "lease.release")]
618 LeaseRelease { set: String },
619 #[serde(rename = "lease.renew")]
620 LeaseRenew { set: String, ttl_ms: i64 },
621 #[serde(rename = "lease.interest")]
625 LeaseInterest {
626 items: Vec<LeaseItem>,
627 intent: String,
628 },
629 #[serde(rename = "change.propose")]
630 ChangePropose {
631 change: ChangeId,
632 commit: GitId,
633 base: GitId,
634 claim: Claim,
635 },
636 #[serde(rename = "change.withdraw")]
637 ChangeWithdraw { change: ChangeId },
638 #[serde(rename = "change.integrate")]
639 ChangeIntegrate {
640 change: ChangeId,
641 trunk_before: GitId,
642 merged: Option<GitId>,
644 conflicts: Vec<String>,
645 },
646 #[serde(rename = "change.integrate_batch")]
651 ChangeIntegrateBatch {
652 trunk_before: GitId,
653 merged: Vec<(ChangeId, GitId)>,
655 },
656 #[serde(rename = "approve")]
660 Approve {
661 target_seq: u64,
662 note: String,
663 #[serde(default, skip_serializing_if = "Vec::is_empty")]
664 checks: Vec<String>,
665 },
666 #[serde(rename = "reject")]
670 Reject { target_seq: u64, reason: String },
671 #[serde(rename = "observe")]
672 Observe {
673 session: String,
674 events: Vec<Observation>,
675 },
676 #[serde(rename = "taint")]
677 Taint { session: String, reason: String },
678 #[serde(rename = "import.external")]
679 ImportExternal {
680 remote: String,
681 commit: GitId,
682 trunk_before: GitId,
683 },
684 #[serde(rename = "import.unmediated")]
685 ImportUnmediated {
686 refname: String,
687 found: Option<GitId>,
688 expected: Option<GitId>,
689 },
690 #[serde(rename = "bridge.push")]
691 BridgePush {
692 remote: String,
693 oplog_seq: u64,
694 oplog_hash: String,
695 },
696 #[serde(rename = "freeze")]
697 Freeze { reason: String },
698 #[serde(rename = "unfreeze")]
699 Unfreeze { reason: String },
700 #[serde(rename = "negotiate")]
701 Negotiate {
702 thread: String,
703 to_session: String,
704 message: NegotiationMsg,
705 },
706 #[serde(rename = "redact")]
707 Redact { blob: GitId, reason: String },
708}
709
710impl Op {
711 pub fn kind(&self) -> OpKind {
712 match self {
713 Op::RepoInit { .. } => OpKind::RepoInit,
714 Op::HumanEnroll { .. } => OpKind::HumanEnroll,
715 Op::HumanRotate { .. } => OpKind::HumanRotate,
716 Op::HumanRevoke { .. } => OpKind::HumanRevoke,
717 Op::HumanRecoveryKey { .. } => OpKind::HumanRecoveryKey,
718 Op::HumanRecover { .. } => OpKind::HumanRecover,
719 Op::DaemonRotate { .. } => OpKind::DaemonRotate,
720 Op::DaemonRecover { .. } => OpKind::DaemonRecover,
721 Op::PolicyUpdate { .. } => OpKind::PolicyUpdate,
722 Op::SessionStart { .. } => OpKind::SessionStart,
723 Op::SessionBind { .. } => OpKind::SessionBind,
724 Op::SessionIsolate { .. } => OpKind::SessionIsolate,
725 Op::SessionEnd { .. } => OpKind::SessionEnd,
726 Op::CapAttenuate { .. } => OpKind::CapAttenuate,
727 Op::CapRevoke { .. } => OpKind::CapRevoke,
728 Op::LeaseAcquire { .. } => OpKind::LeaseAcquire,
729 Op::LeaseRelease { .. } => OpKind::LeaseRelease,
730 Op::LeaseRenew { .. } => OpKind::LeaseRenew,
731 Op::LeaseInterest { .. } => OpKind::LeaseInterest,
732 Op::ChangePropose { .. } => OpKind::ChangePropose,
733 Op::ChangeWithdraw { .. } => OpKind::ChangeWithdraw,
734 Op::ChangeIntegrate { .. } => OpKind::ChangeIntegrate,
735 Op::ChangeIntegrateBatch { .. } => OpKind::ChangeIntegrateBatch,
736 Op::Approve { .. } => OpKind::Approve,
737 Op::Reject { .. } => OpKind::Reject,
738 Op::Observe { .. } => OpKind::Observe,
739 Op::Taint { .. } => OpKind::Taint,
740 Op::ImportExternal { .. } => OpKind::ImportExternal,
741 Op::ImportUnmediated { .. } => OpKind::ImportUnmediated,
742 Op::BridgePush { .. } => OpKind::BridgePush,
743 Op::Freeze { .. } => OpKind::Freeze,
744 Op::Unfreeze { .. } => OpKind::Unfreeze,
745 Op::Negotiate { .. } => OpKind::Negotiate,
746 Op::Redact { .. } => OpKind::Redact,
747 }
748 }
749
750 pub fn daemon_key_switch(&self) -> Option<&str> {
752 match self {
753 Op::DaemonRotate { key } | Op::DaemonRecover { key } => Some(key),
754 _ => None,
755 }
756 }
757}
758
759#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
760#[serde(rename_all = "snake_case")]
761pub enum Outcome {
762 Allow,
763 Deny,
764 Escalate,
765}
766
767#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
768pub struct Decision {
769 pub outcome: Outcome,
770 pub reasons: Vec<String>,
772}
773
774impl Decision {
775 pub fn allow() -> Self {
776 Self {
777 outcome: Outcome::Allow,
778 reasons: vec![],
779 }
780 }
781
782 pub fn deny(reason: impl Into<String>) -> Self {
783 Self {
784 outcome: Outcome::Deny,
785 reasons: vec![reason.into()],
786 }
787 }
788
789 pub fn escalate(reason: impl Into<String>) -> Self {
790 Self {
791 outcome: Outcome::Escalate,
792 reasons: vec![reason.into()],
793 }
794 }
795
796 pub fn is_allow(&self) -> bool {
797 self.outcome == Outcome::Allow
798 }
799}
800
801#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
804pub struct HumanSignature {
805 pub human: String,
806 pub nonce: String,
807 pub issued_ms: i64,
808 pub sig: String,
809}
810
811#[derive(Serialize)]
812pub struct HumanIntent<'a> {
813 pub purpose: &'static str,
814 pub repo: &'a str,
815 pub op: &'a Op,
816 pub nonce: &'a str,
817 pub issued_ms: i64,
818}
819
820impl<'a> HumanIntent<'a> {
821 pub fn new(repo: &'a str, op: &'a Op, nonce: &'a str, issued_ms: i64) -> Self {
822 Self {
823 purpose: "lonca-human-intent-v1",
824 repo,
825 op,
826 nonce,
827 issued_ms,
828 }
829 }
830}
831
832#[derive(Serialize)]
836pub struct LoginIntent<'a> {
837 pub purpose: &'static str,
838 pub repo: Option<&'a str>,
841 pub human: &'a str,
842 pub ttl_ms: i64,
843 pub nonce: &'a str,
844 pub issued_ms: i64,
845}
846
847impl<'a> LoginIntent<'a> {
848 pub fn new(
849 repo: Option<&'a str>,
850 human: &'a str,
851 ttl_ms: i64,
852 nonce: &'a str,
853 issued_ms: i64,
854 ) -> Self {
855 Self {
856 purpose: "lonca-human-login-v1",
857 repo,
858 human,
859 ttl_ms,
860 nonce,
861 issued_ms,
862 }
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869
870 #[test]
872 fn lease_kinds_are_named_as_they_are_written() {
873 for kind in LeaseKind::ALL {
874 let written = serde_json::to_value(kind).unwrap();
875 assert_eq!(written, kind.as_str());
876 assert_eq!(LeaseKind::from_name(kind.as_str()), Some(kind));
877 }
878 assert_eq!(LeaseKind::from_name("Shared"), None);
879 assert_eq!(LeaseKind::from_name(""), None);
880 }
881
882 #[test]
883 fn a_change_reaches_others_by_any_one_of_three() {
884 assert!(!Facts::default().reaches_others());
885 let signature = Facts {
886 signature_changes: vec!["a.rs#f".into()],
887 ..Facts::default()
888 };
889 let mentioned = Facts {
890 affected: vec!["a.rs#g".into()],
891 ..Facts::default()
892 };
893 let cut_off = Facts {
894 affected_more: 1,
895 ..Facts::default()
896 };
897 assert!(signature.reaches_others());
898 assert!(mentioned.reaches_others());
899 assert!(cut_off.reaches_others());
900 }
901
902 #[test]
904 fn what_was_cut_from_affected_is_written_out() {
905 let none = serde_json::to_string(&Facts::default()).unwrap();
906 assert!(!none.contains("affected"), "{none}");
907 let cut_off = Facts {
908 affected_more: 3,
909 ..Facts::default()
910 };
911 let text = serde_json::to_string(&cut_off).unwrap();
912 assert!(text.contains("\"affected_more\":3"), "{text}");
913 }
914
915 #[test]
916 fn unknown_op_kind_is_rejected() {
917 assert!(serde_json::from_str::<OpKind>("\"change.propose\"").is_ok());
918 assert!(serde_json::from_str::<OpKind>("\"admin.everything\"").is_err());
919 }
920
921 #[test]
922 fn op_kind_names_match_serde() {
923 for k in [
924 OpKind::RepoInit,
925 OpKind::ChangePropose,
926 OpKind::LeaseAcquire,
927 OpKind::Redact,
928 OpKind::SessionBind,
929 OpKind::SessionIsolate,
930 OpKind::HumanRotate,
931 OpKind::HumanRevoke,
932 OpKind::HumanRecoveryKey,
933 OpKind::HumanRecover,
934 OpKind::DaemonRotate,
935 OpKind::DaemonRecover,
936 ] {
937 assert_eq!(
938 serde_json::to_string(&k).unwrap(),
939 format!("\"{}\"", k.as_str())
940 );
941 }
942 }
943
944 #[test]
945 fn reserved_ops_are_not_grantable() {
946 for k in [
947 OpKind::PolicyUpdate,
948 OpKind::Approve,
949 OpKind::CapRevoke,
950 OpKind::Redact,
951 OpKind::HumanEnroll,
952 OpKind::HumanRotate,
953 OpKind::HumanRevoke,
954 OpKind::HumanRecoveryKey,
955 OpKind::HumanRecover,
956 OpKind::DaemonRotate,
957 OpKind::DaemonRecover,
958 OpKind::Freeze,
959 OpKind::ImportExternal,
960 ] {
961 assert!(!k.agent_grantable(), "{k:?}");
962 }
963 }
964}