1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum SyncClass {
42 Portable,
44 DeviceLocal,
46}
47
48impl SyncClass {
49 pub fn is_portable(self) -> bool {
50 matches!(self, SyncClass::Portable)
51 }
52}
53
54#[derive(Debug, Clone, Copy)]
56pub struct SurfacePolicy {
57 pub domain: &'static str,
59 pub class: SyncClass,
60 pub note: &'static str,
62}
63
64use SyncClass::{DeviceLocal, Portable};
65
66pub const SURFACE_POLICIES: &[SurfacePolicy] = &[
69 SurfacePolicy {
71 domain: "agent_permissions",
72 class: Portable,
73 note: "per-agent × per-tier posture (always-allow/require-approval/deny) — OS-agnostic intent; \
74 the OS-level grant it may reference is device-local and resolved per platform",
75 },
76 SurfacePolicy {
77 domain: "messaging_allowlist",
78 class: Portable,
79 note: "which handles/channels may approve — policy, not credentials",
80 },
81 SurfacePolicy {
82 domain: "agent_definitions",
83 class: Portable,
84 note: "declarative agents (agents.json / declagents) — portable definitions",
85 },
86 SurfacePolicy {
87 domain: "routing_priors",
88 class: Portable,
89 note: "learned capability-routing success priors — improves every device",
90 },
91 SurfacePolicy {
92 domain: "memory_graph",
93 class: Portable,
94 note: "the knowledge/identity/skill graph — the point of shared memory across devices",
95 },
96 SurfacePolicy {
97 domain: "approvals_ledger",
98 class: Portable,
99 note: "HITL decisions keyed by a stable fingerprint — a decision made on one device is \
100 honored on all (no re-prompting the same hazard per machine)",
101 },
102 SurfacePolicy {
104 domain: "parslee_tokens",
105 class: DeviceLocal,
106 note: "each device mints its own via login (correct); a token is a secret and per-device",
107 },
108 SurfacePolicy {
109 domain: "keychain_secrets",
110 class: DeviceLocal,
111 note: "Slack bot/app tokens et al. live in the OS keychain — a secret; the portable \
112 messaging config references them by a device-local keychain ref, re-provisioned per device",
113 },
114 SurfacePolicy {
115 domain: "permission_grants",
116 class: DeviceLocal,
117 note: "the OS's actual yes/no for a capability (macOS TCC, etc.) — re-obtained per device; \
118 syncing a grant across OSes is meaningless and unsafe",
119 },
120 SurfacePolicy {
121 domain: "machine_paths",
122 class: DeviceLocal,
123 note: "cwd/home/worktree/model-cache paths differ per machine and OS",
124 },
125 SurfacePolicy {
126 domain: "device_pairing",
127 class: DeviceLocal,
128 note: "the messaging pairing code binds one channel to this device",
129 },
130 SurfacePolicy {
131 domain: "voiceprints",
132 class: DeviceLocal,
133 note: "voice enrollment is bound to this device's microphone/enrollment audio",
134 },
135];
136
137pub fn policy_for(domain: &str) -> Option<&'static SurfacePolicy> {
139 SURFACE_POLICIES.iter().find(|p| p.domain == domain)
140}
141
142pub fn is_portable(domain: &str) -> bool {
146 policy_for(domain)
147 .map(|p| p.class.is_portable())
148 .unwrap_or(false)
149}
150
151pub fn portable_domains() -> impl Iterator<Item = &'static str> {
153 SURFACE_POLICIES
154 .iter()
155 .filter(|p| p.class.is_portable())
156 .map(|p| p.domain)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn secrets_and_os_grants_are_never_portable() {
165 for d in [
168 "parslee_tokens",
169 "keychain_secrets",
170 "permission_grants",
171 "machine_paths",
172 "device_pairing",
173 "voiceprints",
174 ] {
175 assert!(!is_portable(d), "{d} must stay device-local");
176 }
177 }
178
179 #[test]
180 fn authored_policy_is_portable() {
181 for d in [
182 "agent_permissions",
183 "messaging_allowlist",
184 "agent_definitions",
185 "routing_priors",
186 "memory_graph",
187 "approvals_ledger",
188 ] {
189 assert!(is_portable(d), "{d} is authored policy — should sync");
190 }
191 }
192
193 #[test]
194 fn unknown_domains_fail_closed_to_device_local() {
195 assert!(!is_portable("some_new_unclassified_surface"));
197 assert!(policy_for("some_new_unclassified_surface").is_none());
198 }
199
200 #[test]
201 fn every_policy_has_a_rationale() {
202 for p in SURFACE_POLICIES {
203 assert!(!p.note.is_empty(), "{} needs a rationale", p.domain);
204 }
205 assert_eq!(portable_domains().count(), 6);
207 }
208}