1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//! Channel identity + the inbound-channel seam (channel-agnostic, Unit 1/2/3).
//!
//! This module is the **cross-platform** spine of the multi-channel approval
//! transport. It defines, with NO `#[cfg(target_os = ...)]` gate anywhere:
//!
//! - [`ChannelId`] — the closed enum naming each approval channel
//! (`IMessage`, `Slack`). Serialized as a stable lowercase string key
//! (`"imessage"` / `"slack"`) so it is a deterministic [`std::collections::BTreeMap`]
//! key in the per-channel config (Unit 2) and a stable wire token (Unit 6).
//! - [`ChannelConfig`] — the per-channel trust state (today's three iMessage
//! fields, now per channel): `enabled`, `allowlisted_handles`,
//! `active_pairing_code`.
//! - [`InboundChannel`] — the **inbound seam** (the missing half #403 never
//! abstracted). A thin object-safe trait exposing `channel()` + an async
//! `run(sink, cancel)`: each adapter OWNS its own loop and feeds the
//! channel-agnostic delivery sink. The shared boundary is message
//! **delivery**, not retrieval — so a poll-based source (iMessage) and a
//! push-based source (Slack) both fit without either faking the other's
//! change-detection model (the `inbound-delivery-model` technical call).
//!
//! Cross-platform on purpose (`macos-cfg-gating` call): the registry, the
//! trait, `ChannelId`, and `ChannelConfig` compile on every platform. Only the
//! iMessage ADAPTER impl + its chat.db readers carry `#[cfg(target_os =
//! "macos")]`. There are NO cargo feature flags (CLAUDE.md hard rule #1).
use async_trait;
use ;
use crateHostState;
use InboundMessage;
/// The closed set of approval channels. **Exhaustively matched everywhere** —
/// no `_ =>` wildcard (CLAUDE.md rule #2), so a future channel forces every
/// match site to be revisited rather than silently swallowed.
///
/// Serialized as a stable lowercase string (`"imessage"` / `"slack"`) so it is
/// a deterministic `BTreeMap` key on disk (Unit 2) and a back-compat wire token
/// (Unit 6 adds the optional `channel` request field, defaulting to
/// `IMessage`). `Ord` is derived for the `BTreeMap` key ordering, giving
/// `messaging.json` a deterministic channel order regardless of insert order.
/// A persisted, serializable reference to a channel's tokens in the OS keychain
/// (MC-9). It carries ONLY the keychain key NAMES the bearer values live under,
/// never the `xoxb-`/`xapp-` strings themselves — so `messaging.json` holds a
/// token *reference*, not a token. Its presence in a [`ChannelConfig`] doubles
/// as the "tokens have been provisioned" marker (the adapter + UI can tell that
/// credentials exist without reading them).
///
/// This is the on-disk twin of the in-memory
/// [`crate::slack_adapter::SlackTokenRefs`] the provisioning write path returns;
/// they hold the same key names but this one is `Serialize`/`Deserialize` for
/// the durable config.
/// Per-channel trust state. The first three fields are exactly #403's iMessage
/// config (`enabled`, `allowlisted_handles`, `active_pairing_code`) — now held
/// once per channel under
/// [`crate::messaging_config::MessagingConfig::channels`]. The Slack-only
/// `slack_token_ref` holds the keychain REFERENCE for that channel's provisioned
/// tokens (MC-9 — a ref, never a bearer), `None` for iMessage / an
/// un-provisioned Slack channel.
///
/// `Default` is **fail-closed**: `enabled = false` (MC-5 — both channels start
/// disabled), empty allowlist, no pairing code, no token ref.
/// The cancel signal the boot path flips on daemon shutdown. A
/// `tokio::sync::watch::Receiver<bool>` set to `true` tells an adapter's
/// `run()` loop to stop. Shared shape across channels (iMessage poll loop,
/// Slack reconnect loop).
pub type CancelSignal = Receiver;
/// The channel-agnostic inbound DELIVERY sink. An adapter feeds each inbound
/// message it observes to `deliver`; the sink routes it to the host's approval
/// semantics (the iMessage adapter wires this to its `handle_inbound`). The
/// boundary is delivery, NOT retrieval — `InboundMessage` carries only what
/// `handle_inbound` reads (`handle_id` + `body`); the watermark stays private to
/// the iMessage adapter's poll loop and never crosses this sink.
/// The **inbound seam** — the abstraction #403 never had. Each channel adapter
/// implements this: it names its [`ChannelId`] and OWNS its own run loop,
/// feeding observed messages into the supplied [`InboundSink`]. Object-safe via
/// `#[async_trait]` (the cross-platform registry holds `Box<dyn InboundChannel>`).
///
/// `run()` is async — each adapter drives its own cadence: iMessage polls on a
/// bounded interval; Slack holds a Socket Mode WebSocket. Neither fakes the
/// other's model.
/// Cross-platform spawn handle bundle: the cancel sender the boot path holds to
/// stop every spawned adapter loop on shutdown. Returned by the registry boot.
/// Reference to the shared host state an adapter resolves approvals against.
/// Kept here (cross-platform) so the registry signature does not leak any
/// macOS-only type. The iMessage adapter wraps this in its own
/// `MessagingOrchestrator`.
pub type SharedHost = Arc;