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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
367 pub taken_keys: Vec<String>,
368}
369
370fn is_zero(n: &u32) -> bool {
371 *n == 0
372}
373
374impl Facts {
375 pub fn reaches_others(&self) -> bool {
378 !self.signature_changes.is_empty() || !self.affected.is_empty() || self.affected_more > 0
379 }
380}
381
382#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384pub enum CheckKind {
385 Syntax,
386 Scope,
387 ClaimDiff,
388 Merge,
389 Build,
390 Tests,
391 TestIntegrity,
392 Extended,
394 Named,
396}
397
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "snake_case")]
400pub enum CheckResult {
401 Pass,
402 Fail,
403 Error,
404 Skipped,
405}
406
407#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
410pub struct Attestation {
411 pub check: CheckKind,
412 pub result: CheckResult,
413 pub subject_tree: GitId,
414 pub details: BTreeMap<String, String>,
415 pub env: String,
423 pub log: Option<GitId>,
424 pub producer: String,
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "snake_case")]
429pub enum Phase {
430 Pre,
431 Post,
432}
433
434#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
435#[serde(tag = "type", rename_all = "snake_case")]
436pub enum ObsKind {
437 Read {
438 path: String,
439 },
440 Write {
441 path: String,
442 },
443 Shell {
446 program: String,
447 digest: String,
448 },
449 Fetch {
450 host: String,
451 },
452 Mcp {
453 tool: String,
454 },
455 Other {
456 tool: String,
457 },
458 Heartbeat,
459 Unproposed {
464 paths: Vec<String>,
465 total: u64,
466 },
467 Unhooked {
471 tool: String,
472 },
473}
474
475pub const MAX_UNPROPOSED_PATHS: usize = 32;
477
478#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
479pub struct Observation {
480 pub phase: Phase,
481 pub kind: ObsKind,
482 pub external: bool,
484}
485
486#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
487#[serde(tag = "type", rename_all = "snake_case")]
488pub enum NegotiationMsg {
489 Claim {
491 symbols: Vec<String>,
492 reason: String,
493 },
494 ProposeSplit {
496 mine: Vec<String>,
497 yours: Vec<String>,
498 },
499 Yield {
503 set: String,
504 #[serde(default, skip_serializing_if = "Option::is_none")]
505 to: Option<String>,
506 },
507 Counter {
508 note: String,
509 },
510 Accept,
511 Decline {
512 note: String,
513 },
514}
515
516#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
517pub struct HumanKey {
518 pub alg: String,
521 pub public: String,
522}
523
524#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
528pub struct Recovery {
529 pub human: String,
530 pub key: HumanKey,
531}
532
533#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
535pub struct Continuation {
536 pub session: String,
537 pub widen: bool,
539}
540
541#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(tag = "op")]
543pub enum Op {
544 #[serde(rename = "repo.init")]
545 RepoInit {
546 repo: String,
547 daemon_key: String,
548 human: String,
549 human_key: HumanKey,
550 policy: GitId,
551 trunk: GitId,
552 },
553 #[serde(rename = "human.enroll")]
554 HumanEnroll { human: String, key: HumanKey },
555 #[serde(rename = "human.rotate")]
557 HumanRotate { human: String, key: HumanKey },
558 #[serde(rename = "human.revoke")]
560 HumanRevoke { human: String, reason: String },
561 #[serde(rename = "human.recovery_key")]
564 HumanRecoveryKey { human: String, key: HumanKey },
565 #[serde(rename = "human.recover")]
568 HumanRecover { human: String, key: HumanKey },
569 #[serde(rename = "daemon.rotate")]
572 DaemonRotate { key: String },
573 #[serde(rename = "daemon.recover")]
579 DaemonRecover {
580 key: String,
581 #[serde(default, skip_serializing_if = "Option::is_none")]
582 recover: Option<Recovery>,
583 },
584 #[serde(rename = "policy.update")]
585 PolicyUpdate { policy: GitId },
586 #[serde(rename = "session.start")]
587 SessionStart {
588 session: String,
589 agent: String,
590 model: Option<String>,
591 task: String,
592 scope: Scope,
593 expires_ms: i64,
594 #[serde(default, skip_serializing_if = "Option::is_none")]
596 continues: Option<Continuation>,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
600 harness: Option<String>,
601 },
602 #[serde(rename = "session.bind")]
603 SessionBind {
604 session: String,
605 checks: BindChecks,
606 level: ObservationLevel,
607 },
608 #[serde(rename = "session.isolate")]
613 SessionIsolate {
614 session: String,
615 sandbox: String,
617 profile: String,
619 },
620 #[serde(rename = "session.end")]
621 SessionEnd { session: String, reason: String },
622 #[serde(rename = "cap.attenuate")]
623 CapAttenuate {
624 parent: String,
625 child: String,
626 agent: String,
627 scope: Scope,
628 expires_ms: i64,
629 #[serde(default, skip_serializing_if = "Vec::is_empty")]
632 leases: Vec<String>,
633 },
634 #[serde(rename = "cap.revoke")]
635 CapRevoke { session: String, reason: String },
636 #[serde(rename = "lease.acquire")]
637 LeaseAcquire {
638 set: String,
639 items: Vec<LeaseItem>,
640 intent: String,
641 ttl_ms: i64,
642 },
643 #[serde(rename = "lease.release")]
644 LeaseRelease { set: String },
645 #[serde(rename = "lease.renew")]
646 LeaseRenew { set: String, ttl_ms: i64 },
647 #[serde(rename = "lease.interest")]
651 LeaseInterest {
652 items: Vec<LeaseItem>,
653 intent: String,
654 },
655 #[serde(rename = "change.propose")]
656 ChangePropose {
657 change: ChangeId,
658 commit: GitId,
659 base: GitId,
660 claim: Claim,
661 },
662 #[serde(rename = "change.withdraw")]
663 ChangeWithdraw { change: ChangeId },
664 #[serde(rename = "change.integrate")]
665 ChangeIntegrate {
666 change: ChangeId,
667 trunk_before: GitId,
668 merged: Option<GitId>,
670 conflicts: Vec<String>,
671 },
672 #[serde(rename = "change.integrate_batch")]
677 ChangeIntegrateBatch {
678 trunk_before: GitId,
679 merged: Vec<(ChangeId, GitId)>,
681 },
682 #[serde(rename = "approve")]
686 Approve {
687 target_seq: u64,
688 note: String,
689 #[serde(default, skip_serializing_if = "Vec::is_empty")]
690 checks: Vec<String>,
691 },
692 #[serde(rename = "reject")]
696 Reject { target_seq: u64, reason: String },
697 #[serde(rename = "observe")]
698 Observe {
699 session: String,
700 events: Vec<Observation>,
701 },
702 #[serde(rename = "taint")]
703 Taint { session: String, reason: String },
704 #[serde(rename = "import.external")]
705 ImportExternal {
706 remote: String,
707 commit: GitId,
708 trunk_before: GitId,
709 },
710 #[serde(rename = "import.unmediated")]
711 ImportUnmediated {
712 refname: String,
713 found: Option<GitId>,
714 expected: Option<GitId>,
715 },
716 #[serde(rename = "bridge.push")]
717 BridgePush {
718 remote: String,
719 oplog_seq: u64,
720 oplog_hash: String,
721 },
722 #[serde(rename = "freeze")]
723 Freeze { reason: String },
724 #[serde(rename = "unfreeze")]
725 Unfreeze { reason: String },
726 #[serde(rename = "negotiate")]
727 Negotiate {
728 thread: String,
729 to_session: String,
730 message: NegotiationMsg,
731 },
732 #[serde(rename = "redact")]
733 Redact { blob: GitId, reason: String },
734}
735
736impl Op {
737 pub fn kind(&self) -> OpKind {
738 match self {
739 Op::RepoInit { .. } => OpKind::RepoInit,
740 Op::HumanEnroll { .. } => OpKind::HumanEnroll,
741 Op::HumanRotate { .. } => OpKind::HumanRotate,
742 Op::HumanRevoke { .. } => OpKind::HumanRevoke,
743 Op::HumanRecoveryKey { .. } => OpKind::HumanRecoveryKey,
744 Op::HumanRecover { .. } => OpKind::HumanRecover,
745 Op::DaemonRotate { .. } => OpKind::DaemonRotate,
746 Op::DaemonRecover { .. } => OpKind::DaemonRecover,
747 Op::PolicyUpdate { .. } => OpKind::PolicyUpdate,
748 Op::SessionStart { .. } => OpKind::SessionStart,
749 Op::SessionBind { .. } => OpKind::SessionBind,
750 Op::SessionIsolate { .. } => OpKind::SessionIsolate,
751 Op::SessionEnd { .. } => OpKind::SessionEnd,
752 Op::CapAttenuate { .. } => OpKind::CapAttenuate,
753 Op::CapRevoke { .. } => OpKind::CapRevoke,
754 Op::LeaseAcquire { .. } => OpKind::LeaseAcquire,
755 Op::LeaseRelease { .. } => OpKind::LeaseRelease,
756 Op::LeaseRenew { .. } => OpKind::LeaseRenew,
757 Op::LeaseInterest { .. } => OpKind::LeaseInterest,
758 Op::ChangePropose { .. } => OpKind::ChangePropose,
759 Op::ChangeWithdraw { .. } => OpKind::ChangeWithdraw,
760 Op::ChangeIntegrate { .. } => OpKind::ChangeIntegrate,
761 Op::ChangeIntegrateBatch { .. } => OpKind::ChangeIntegrateBatch,
762 Op::Approve { .. } => OpKind::Approve,
763 Op::Reject { .. } => OpKind::Reject,
764 Op::Observe { .. } => OpKind::Observe,
765 Op::Taint { .. } => OpKind::Taint,
766 Op::ImportExternal { .. } => OpKind::ImportExternal,
767 Op::ImportUnmediated { .. } => OpKind::ImportUnmediated,
768 Op::BridgePush { .. } => OpKind::BridgePush,
769 Op::Freeze { .. } => OpKind::Freeze,
770 Op::Unfreeze { .. } => OpKind::Unfreeze,
771 Op::Negotiate { .. } => OpKind::Negotiate,
772 Op::Redact { .. } => OpKind::Redact,
773 }
774 }
775
776 pub fn signed_by_recovery_key(&self) -> bool {
779 matches!(
780 self,
781 Op::HumanRecover { .. }
782 | Op::DaemonRecover {
783 recover: Some(_),
784 ..
785 }
786 )
787 }
788
789 pub fn daemon_key_switch(&self) -> Option<&str> {
791 match self {
792 Op::DaemonRotate { key } | Op::DaemonRecover { key, .. } => Some(key),
793 _ => None,
794 }
795 }
796}
797
798#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(rename_all = "snake_case")]
800pub enum Outcome {
801 Allow,
802 Deny,
803 Escalate,
804}
805
806#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
807pub struct Decision {
808 pub outcome: Outcome,
809 pub reasons: Vec<String>,
811}
812
813impl Decision {
814 pub fn allow() -> Self {
815 Self {
816 outcome: Outcome::Allow,
817 reasons: vec![],
818 }
819 }
820
821 pub fn deny(reason: impl Into<String>) -> Self {
822 Self {
823 outcome: Outcome::Deny,
824 reasons: vec![reason.into()],
825 }
826 }
827
828 pub fn escalate(reason: impl Into<String>) -> Self {
829 Self {
830 outcome: Outcome::Escalate,
831 reasons: vec![reason.into()],
832 }
833 }
834
835 pub fn is_allow(&self) -> bool {
836 self.outcome == Outcome::Allow
837 }
838}
839
840#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
843pub struct HumanSignature {
844 pub human: String,
845 pub nonce: String,
846 pub issued_ms: i64,
847 pub sig: String,
848}
849
850#[derive(Serialize)]
851pub struct HumanIntent<'a> {
852 pub purpose: &'static str,
853 pub repo: &'a str,
854 pub op: &'a Op,
855 pub nonce: &'a str,
856 pub issued_ms: i64,
857}
858
859impl<'a> HumanIntent<'a> {
860 pub fn new(repo: &'a str, op: &'a Op, nonce: &'a str, issued_ms: i64) -> Self {
861 Self {
862 purpose: "lonca-human-intent-v1",
863 repo,
864 op,
865 nonce,
866 issued_ms,
867 }
868 }
869}
870
871#[derive(Serialize)]
875pub struct LoginIntent<'a> {
876 pub purpose: &'static str,
877 pub repo: Option<&'a str>,
880 pub human: &'a str,
881 pub ttl_ms: i64,
882 pub nonce: &'a str,
883 pub issued_ms: i64,
884}
885
886impl<'a> LoginIntent<'a> {
887 pub fn new(
888 repo: Option<&'a str>,
889 human: &'a str,
890 ttl_ms: i64,
891 nonce: &'a str,
892 issued_ms: i64,
893 ) -> Self {
894 Self {
895 purpose: "lonca-human-login-v1",
896 repo,
897 human,
898 ttl_ms,
899 nonce,
900 issued_ms,
901 }
902 }
903}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908
909 #[test]
911 fn lease_kinds_are_named_as_they_are_written() {
912 for kind in LeaseKind::ALL {
913 let written = serde_json::to_value(kind).unwrap();
914 assert_eq!(written, kind.as_str());
915 assert_eq!(LeaseKind::from_name(kind.as_str()), Some(kind));
916 }
917 assert_eq!(LeaseKind::from_name("Shared"), None);
918 assert_eq!(LeaseKind::from_name(""), None);
919 }
920
921 #[test]
922 fn a_change_reaches_others_by_any_one_of_three() {
923 assert!(!Facts::default().reaches_others());
924 let signature = Facts {
925 signature_changes: vec!["a.rs#f".into()],
926 ..Facts::default()
927 };
928 let mentioned = Facts {
929 affected: vec!["a.rs#g".into()],
930 ..Facts::default()
931 };
932 let cut_off = Facts {
933 affected_more: 1,
934 ..Facts::default()
935 };
936 assert!(signature.reaches_others());
937 assert!(mentioned.reaches_others());
938 assert!(cut_off.reaches_others());
939 }
940
941 #[test]
943 fn what_was_cut_from_affected_is_written_out() {
944 let none = serde_json::to_string(&Facts::default()).unwrap();
945 assert!(!none.contains("affected"), "{none}");
946 let cut_off = Facts {
947 affected_more: 3,
948 ..Facts::default()
949 };
950 let text = serde_json::to_string(&cut_off).unwrap();
951 assert!(text.contains("\"affected_more\":3"), "{text}");
952 }
953
954 #[test]
955 fn unknown_op_kind_is_rejected() {
956 assert!(serde_json::from_str::<OpKind>("\"change.propose\"").is_ok());
957 assert!(serde_json::from_str::<OpKind>("\"admin.everything\"").is_err());
958 }
959
960 #[test]
961 fn op_kind_names_match_serde() {
962 for k in [
963 OpKind::RepoInit,
964 OpKind::ChangePropose,
965 OpKind::LeaseAcquire,
966 OpKind::Redact,
967 OpKind::SessionBind,
968 OpKind::SessionIsolate,
969 OpKind::HumanRotate,
970 OpKind::HumanRevoke,
971 OpKind::HumanRecoveryKey,
972 OpKind::HumanRecover,
973 OpKind::DaemonRotate,
974 OpKind::DaemonRecover,
975 ] {
976 assert_eq!(
977 serde_json::to_string(&k).unwrap(),
978 format!("\"{}\"", k.as_str())
979 );
980 }
981 }
982
983 #[test]
984 fn reserved_ops_are_not_grantable() {
985 for k in [
986 OpKind::PolicyUpdate,
987 OpKind::Approve,
988 OpKind::CapRevoke,
989 OpKind::Redact,
990 OpKind::HumanEnroll,
991 OpKind::HumanRotate,
992 OpKind::HumanRevoke,
993 OpKind::HumanRecoveryKey,
994 OpKind::HumanRecover,
995 OpKind::DaemonRotate,
996 OpKind::DaemonRecover,
997 OpKind::Freeze,
998 OpKind::ImportExternal,
999 ] {
1000 assert!(!k.agent_grantable(), "{k:?}");
1001 }
1002 }
1003}