Skip to main content

rightkit_agent_engine_protocol/
protocol.rs

1//! Approval policy contracts ported from Codex's portable protocol.
2//!
3//! DIRECT_PORT_RUST (atom `S:I4`): ported verbatim from codex
4//! `codex-rs/protocol/src/protocol.rs:963` (`AskForApproval`,
5//! `SandboxPolicy`'s network-access leg re-cast here as `NetworkAccess`) plus
6//! the granular per-category approval controls added alongside it. See
7//! `docs/canon/daemon-master-canon-atoms.md` (donor-verification correction,
8//! 2026-08-29: Waveloom/Go and Pi/TypeScript are not valid `E→A` sources for
9//! this atom; Codex is the sole donor) for the exact donor revision and
10//! NOTICE entry. No behavioral changes were made at this seam beyond the
11//! module path (`codex_protocol::protocol` -> `rightkit_agent_engine_protocol::protocol`)
12//! and wiring `AskForApproval` onto the host app's own `PermissionMode` ladder
13//! (see `host_policy::types::parse_permission_mode_strict`) rather than
14//! introducing a second approval-mode enum at the seam.
15
16use serde::{Deserialize, Serialize};
17
18/// Determines when an operator is consulted before command execution.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "kebab-case")]
21pub enum AskForApproval {
22    /// Require approval for untrusted projects.
23    #[serde(rename = "untrusted")]
24    UnlessTrusted,
25    /// Ask when policy/model requests it.
26    #[default]
27    OnRequest,
28    /// Apply per-category controls.
29    Granular(GranularApprovalConfig),
30    /// Never ask; return failures directly to model.
31    Never,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub struct GranularApprovalConfig {
36    pub sandbox_approval: bool,
37    pub rules: bool,
38    #[serde(default)]
39    pub skill_approval: bool,
40    #[serde(default)]
41    pub request_permissions: bool,
42    pub mcp_elicitations: bool,
43}
44
45impl GranularApprovalConfig {
46    pub const fn allows_sandbox_approval(self) -> bool {
47        self.sandbox_approval
48    }
49    pub const fn allows_rules_approval(self) -> bool {
50        self.rules
51    }
52    pub const fn allows_skill_approval(self) -> bool {
53        self.skill_approval
54    }
55    pub const fn allows_request_permissions(self) -> bool {
56        self.request_permissions
57    }
58    pub const fn allows_mcp_elicitations(self) -> bool {
59        self.mcp_elicitations
60    }
61}
62
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "kebab-case")]
65pub enum NetworkAccess {
66    #[default]
67    Restricted,
68    Enabled,
69}
70
71impl NetworkAccess {
72    pub const fn is_enabled(self) -> bool {
73        matches!(self, Self::Enabled)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    /// S:I4: `AskForApproval` must keep Codex's exact wire tags -- the
82    /// `#[serde(rename = "untrusted")]` on `UnlessTrusted` is a deliberate
83    /// deviation from the field's own kebab-case name and must not regress
84    /// to the derived `unless-trusted` spelling.
85    #[test]
86    fn ask_for_approval_keeps_codex_wire_tags() {
87        assert_eq!(
88            serde_json::to_value(AskForApproval::UnlessTrusted).unwrap(),
89            serde_json::json!("untrusted")
90        );
91        assert_eq!(
92            serde_json::to_value(AskForApproval::OnRequest).unwrap(),
93            serde_json::json!("on-request")
94        );
95        assert_eq!(
96            serde_json::to_value(AskForApproval::Never).unwrap(),
97            serde_json::json!("never")
98        );
99        assert_eq!(
100            serde_json::from_value::<AskForApproval>(serde_json::json!("untrusted")).unwrap(),
101            AskForApproval::UnlessTrusted
102        );
103    }
104
105    #[test]
106    fn ask_for_approval_defaults_to_on_request() {
107        assert_eq!(AskForApproval::default(), AskForApproval::OnRequest);
108    }
109
110    #[test]
111    fn granular_approval_config_round_trips() {
112        let config = GranularApprovalConfig {
113            sandbox_approval: true,
114            rules: false,
115            skill_approval: true,
116            request_permissions: false,
117            mcp_elicitations: true,
118        };
119        let ask = AskForApproval::Granular(config);
120        let value = serde_json::to_value(ask).unwrap();
121        let back: AskForApproval = serde_json::from_value(value).unwrap();
122        assert_eq!(back, ask);
123        assert!(config.allows_sandbox_approval());
124        assert!(!config.allows_rules_approval());
125        assert!(config.allows_skill_approval());
126        assert!(!config.allows_request_permissions());
127        assert!(config.allows_mcp_elicitations());
128    }
129
130    #[test]
131    fn network_access_keeps_kebab_case_wire_tags() {
132        assert_eq!(
133            serde_json::to_value(NetworkAccess::Restricted).unwrap(),
134            serde_json::json!("restricted")
135        );
136        assert_eq!(
137            serde_json::to_value(NetworkAccess::Enabled).unwrap(),
138            serde_json::json!("enabled")
139        );
140        assert!(!NetworkAccess::Restricted.is_enabled());
141        assert!(NetworkAccess::Enabled.is_enabled());
142        assert_eq!(NetworkAccess::default(), NetworkAccess::Restricted);
143    }
144}