Skip to main content

vigil_ui_protocol/
lib.rs

1//! vigil-ui-protocol
2//!
3//! I08a(ADR 0008):framework-agnostic UI protocol —— CLI / Tauri / Web / 测试 harness
4//! 都通过本 crate 的 [`UiCommand`][] / [`UiResponse`][] / [`UiError`][] 交互。
5//!
6//! **安全不变量**(ADR §I-8.1 ~ §I-8.6):
7//! - 协议层**不直接持** `Arc<Ledger>`;dispatcher 是集成层的责任
8//! - `UiError` 所有变种**不含** raw secret / 后端原始错误文本
9//! - 写命令必须 capability=`ui.write`,静态检查
10//! - `SandboxProfile.profile_json` 必须 JCS 规范化后 hash
11
12#![deny(missing_docs)]
13#![forbid(unsafe_code)]
14#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
15
16mod command;
17mod error;
18mod response;
19
20pub use command::{
21    ApprovalAction, ApproveServerCommandDriftReq, ApproveToolDriftReq, ApproveToolReq,
22    BindServerSandboxProfileReq, Capability, ExportFormat, ExportSessionReplayReq, FtsSearchReq,
23    GetApprovalDetailReq, GetEventDetailReq, GetSandboxProfileReq, GetServerOnboardingReq,
24    ListPendingApprovalsReq, ListPrivacyFindingsReq, ListRecentEventsReq, ListSessionsReq,
25    RejectServerCommandDriftReq, RejectToolDriftReq, ReplaySessionReq, ResolveApprovalReq,
26    UiCommand, UpsertSandboxProfileReq,
27};
28pub use error::UiError;
29pub use response::{
30    ApprovalDetailDto, ApprovalResolutionDto, ApprovalSummary, ChainVerifyReport, EventDetail,
31    EventSummary, PrivacyFindingDto, PrivacyFindingsDto, RedactionScanSummaryDto,
32    SandboxProfileUpsertDto, SecretBindingSummary, SessionExportDto, SessionReplay, SessionSummary,
33    UiResponse,
34};
35
36/// 当前迭代号。
37pub const ITERATION: &str = "I08a";
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn required_capability_read_for_all_read_commands() {
45        let reads = [
46            UiCommand::VerifyChain,
47            UiCommand::ListServers,
48            UiCommand::ListPendingToolApprovals,
49            UiCommand::ListDriftedTools,
50            UiCommand::ListDriftedServers,
51            UiCommand::ListSandboxProfiles,
52            UiCommand::ListRecentEvents(Default::default()),
53            UiCommand::GetEventDetail(GetEventDetailReq { event_id: 1 }),
54            UiCommand::FtsSearch(FtsSearchReq {
55                query: "x".into(),
56                limit: 10,
57            }),
58            UiCommand::ListPendingApprovals(Default::default()),
59            UiCommand::GetApprovalDetail(GetApprovalDetailReq {
60                approval_id: "a".into(),
61            }),
62            UiCommand::ListSessions(Default::default()),
63            UiCommand::ReplaySession(ReplaySessionReq {
64                session_id: "s".into(),
65                verify: false,
66            }),
67            UiCommand::GetServerOnboarding(GetServerOnboardingReq {
68                server_id: "s".into(),
69            }),
70            UiCommand::GetSandboxProfile(GetSandboxProfileReq {
71                profile_id: "p".into(),
72            }),
73        ];
74        for c in reads {
75            assert_eq!(
76                c.required_capability(),
77                Capability::Read,
78                "{c:?} should be Read"
79            );
80        }
81    }
82
83    #[test]
84    fn required_capability_write_for_all_write_commands() {
85        use vigil_runner_types::{RunnerKind, RunnerSpecific, SandboxProfile};
86        let writes = [
87            UiCommand::ResolveApproval(ResolveApprovalReq {
88                approval_id: "a".into(),
89                action: ApprovalAction::Approve,
90                scope: None,
91                resolved_by: "u".into(),
92                reason: None,
93            }),
94            UiCommand::ApproveTool(ApproveToolReq {
95                server_id: "s".into(),
96                tool_name: "t".into(),
97            }),
98            UiCommand::ApproveToolDrift(ApproveToolDriftReq {
99                server_id: "s".into(),
100                tool_name: "t".into(),
101                new_hash: "h".into(),
102            }),
103            UiCommand::RejectToolDrift(RejectToolDriftReq {
104                server_id: "s".into(),
105                tool_name: "t".into(),
106            }),
107            UiCommand::ApproveServerCommandDrift(ApproveServerCommandDriftReq {
108                server_id: "s".into(),
109            }),
110            UiCommand::RejectServerCommandDrift(RejectServerCommandDriftReq {
111                server_id: "s".into(),
112            }),
113            UiCommand::UpsertSandboxProfile(UpsertSandboxProfileReq {
114                profile: SandboxProfile {
115                    id: "p".into(),
116                    read_dirs: vec![],
117                    write_dirs: vec![],
118                    allow_hosts: vec![],
119                    env_inherit: false,
120                    wall_ms: 1000,
121                    memory_mb: 64,
122                },
123            }),
124            UiCommand::BindServerSandboxProfile(BindServerSandboxProfileReq {
125                server_id: "s".into(),
126                profile_id: Some("p".into()),
127            }),
128        ];
129        let _ = RunnerKind::Native;
130        let _ = RunnerSpecific::Native {
131            rlimit_placeholder: None,
132        };
133        for c in writes {
134            assert_eq!(
135                c.required_capability(),
136                Capability::Write,
137                "{c:?} should be Write"
138            );
139        }
140    }
141
142    #[test]
143    fn ui_command_serde_roundtrip() {
144        let c = UiCommand::ListRecentEvents(ListRecentEventsReq {
145            session_id: Some("sid".into()),
146            event_type_filter: Some(vec!["decision.recorded".into()]),
147            limit: 100,
148        });
149        let s = serde_json::to_string(&c).unwrap();
150        let back: UiCommand = serde_json::from_str(&s).unwrap();
151        assert_eq!(c, back);
152    }
153}