Skip to main content

car_sync/
partition.rs

1//! What syncs across a user's devices, what stays device-local, and how a
2//! synced policy is reconciled when the devices run different operating systems.
3//!
4//! # The problem
5//!
6//! "Onboard once" must copy the config you *set* — not the secrets and grants
7//! that are bound to a specific machine or OS. Sync the wrong thing and you
8//! either leak a bearer token to every device (and the relay) or push a macOS
9//! TCC grant onto a Linux box where it's meaningless. So every piece of CAR's
10//! on-device state is classified once, here, and the daemon's tee consults this
11//! before it puts anything on the oplog.
12//!
13//! # The two rules
14//!
15//! 1. **Portable = policy you authored.** Which tier each agent gets, which
16//!    handles may approve, your agent definitions, routing priors, the
17//!    knowledge graph, and the HITL decisions you already made (recorded by a
18//!    stable *fingerprint*, so "I approved this" applies on every device). These
19//!    are OS-agnostic *intent* — they ride the E2E oplog and land on every
20//!    device.
21//! 2. **Device-local = secrets and machine/OS-bound facts.** OS keychain
22//!    material (Slack bot tokens, the Parslee tokens themselves), TCC/permission
23//!    *grants* (the OS's yes/no, re-obtained per device), machine paths, the
24//!    device pairing code, and voiceprints (bound to this device's mic). These
25//!    **never** leave the device — syncing them would leak a secret or break on
26//!    a different OS.
27//!
28//! # Cross-OS reconciliation
29//!
30//! A *portable* posture can name a capability that only exists on one OS —
31//! e.g. an agent granted `Automation`/`Calendar`/`HealthRead` (macOS TCC
32//! domains). The posture still syncs verbatim (it's intent). What differs per
33//! device is the **grant**, which is device-local and resolved by the platform
34//! backend: `car-permissions`' Linux/Windows backends already report such a
35//! domain as `NotApplicable`. So the split is clean: sync the *policy*, let each
36//! OS resolve the *capability*. Nothing here re-implements that resolution — it
37//! only guarantees the grant itself is never teed.
38
39/// How a CAR config domain relates to multi-device sync.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum SyncClass {
42    /// OS-agnostic policy/intent — replicates verbatim to every device.
43    Portable,
44    /// A secret or machine/OS-bound fact — never leaves the device.
45    DeviceLocal,
46}
47
48impl SyncClass {
49    pub fn is_portable(self) -> bool {
50        matches!(self, SyncClass::Portable)
51    }
52}
53
54/// A named on-device state domain and its sync classification.
55#[derive(Debug, Clone, Copy)]
56pub struct SurfacePolicy {
57    /// Stable identifier (matches the daemon's config/tee surface name).
58    pub domain: &'static str,
59    pub class: SyncClass,
60    /// Why — the load-bearing rationale (secret? OS-bound? pure policy?).
61    pub note: &'static str,
62}
63
64use SyncClass::{DeviceLocal, Portable};
65
66/// The canonical partition of CAR's on-device state. The daemon's tee syncs
67/// exactly the [`Portable`] domains and never the [`DeviceLocal`] ones.
68pub const SURFACE_POLICIES: &[SurfacePolicy] = &[
69    // --- Portable policy (rides the E2E oplog) ---
70    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    // --- Device-local (never teed) ---
103    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
137/// The policy for `domain`, if known.
138pub fn policy_for(domain: &str) -> Option<&'static SurfacePolicy> {
139    SURFACE_POLICIES.iter().find(|p| p.domain == domain)
140}
141
142/// Does `domain` sync across devices? **Fails closed**: an *unknown* domain is
143/// treated as device-local, so a newly-added config surface never leaks onto
144/// the relay until it's been deliberately classified here.
145pub fn is_portable(domain: &str) -> bool {
146    policy_for(domain)
147        .map(|p| p.class.is_portable())
148        .unwrap_or(false)
149}
150
151/// Every domain that syncs — the tee's allowlist.
152pub 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        // The security-critical invariant: nothing that is a secret or an
166        // OS-bound grant may ride the oplog.
167        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        // A new config surface someone forgot to classify must NOT leak.
196        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        // The tee's allowlist is exactly the portable set.
206        assert_eq!(portable_domains().count(), 6);
207    }
208}