Skip to main content

wire/command/
wire.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Everything a client may ask the city to do, and the two invariants
7//! that live in the type system rather than in a check.
8//!
9//! - `Command` is generic over the carrier of a secret. `WireCommand`
10//!   fixes that carrier to an uninhabited type, so a frame arriving from
11//!   a socket cannot be a `PutSecret` — not "is rejected", but has no
12//!   representation. Credentials are enrolled on the host machine, and
13//!   that constraint is held by construction.
14//! - Every state-changing Command owns an `IdemKey` field. There is no
15//!   constructor that omits it, so "double-clicking twice opens two
16//!   runs" is not reachable from this type.
17//!
18//! The enum is not `#[non_exhaustive]`: the wire version is the
19//! versioning mechanism, so the assembly layer must handle every variant
20//! and a new one fails to compile until somebody decides what it does.
21//! That is what keeps a button off the client until the city can answer
22//! the frame behind it.
23
24//! The wire face: no-secret commands and back.
25
26use kernel::IdemKey;
27
28use super::kind::Command;
29use super::no_secret::NoSecret;
30use super::step::{CitySettings, ProposalDecisions, RangeWrite, RulesWrite};
31
32/// The Command set a socket can carry. `PutSecret` is unreachable because
33/// `NoSecret` has no values.
34pub type WireCommand = Command<NoSecret>;
35
36impl<Secret> Command<Secret> {
37    /// The deduplication key. `None` only for the two Commands that change
38    /// nothing: `Auth`, and `PutSecret` whose effect is confined to the host
39    /// process and whose replay writes the same Vault entry.
40    pub fn idem(&self) -> Option<&IdemKey> {
41        match *self {
42            Self::Dispatch { ref idem, .. }
43            | Self::Wake { ref idem, .. }
44            | Self::OpenSession { ref idem, .. }
45            | Self::ProbeEndpoint { ref idem, .. }
46            | Self::ConfigureBuilding { ref idem, .. }
47            | Self::CreateBuilding { ref idem, .. }
48            | Self::RemoveBuilding { ref idem, .. }
49            | Self::Steer { ref idem, .. }
50            | Self::Cancel { ref idem, .. }
51            | Self::Halt { ref idem, .. }
52            | Self::Release { ref idem, .. }
53            | Self::BatchByBuilding { ref idem, .. }
54            | Self::Approve { ref idem, .. }
55            | Self::HandOff { ref idem, .. }
56            | Self::SetAutonomy { ref idem, .. }
57            | Self::Pursue { ref idem, .. }
58            | Self::PutDocument { ref idem, .. }
59            | Self::PutIdentity { ref idem, .. }
60            | Self::PutRules(RulesWrite { ref idem, .. })
61            | Self::ConfigureCity(CitySettings { ref idem, .. })
62            | Self::RestoreFile { ref idem, .. }
63            | Self::PutGuide { ref idem, .. }
64            | Self::PutSpine { ref idem, .. }
65            | Self::PutRange(RangeWrite { ref idem, .. })
66            | Self::DecideProposals(ProposalDecisions { ref idem, .. })
67            | Self::PutPreferences { ref idem, .. }
68            | Self::PutShelved { ref idem, .. }
69            | Self::AttachEndpoint { ref idem, .. }
70            | Self::SelectModel { ref idem, .. }
71            | Self::Reveal { ref idem, .. }
72            | Self::RestoreDiscard { ref idem, .. }
73            | Self::DoctorInstall { ref idem, .. }
74            | Self::DoctorRefresh { ref idem, .. }
75            | Self::ConnectToolkit { ref idem, .. } => Some(idem),
76            Self::PutSecret { .. } | Self::Auth { .. } => None,
77        }
78    }
79}
80
81impl From<WireCommand> for Command {
82    /// Widening a wire frame into the in-process Command set. Total: the
83    /// `PutSecret` arm is unreachable because its payload cannot exist.
84    fn from(wire: WireCommand) -> Self {
85        match wire {
86            Command::Wake {
87                source,
88                subject,
89                body,
90                idem,
91            } => Self::Wake {
92                source,
93                subject,
94                body,
95                idem,
96            },
97            Command::Dispatch {
98                addr,
99                task,
100                goal,
101                policy,
102                idem,
103                session,
104                effort,
105                model,
106            } => Self::Dispatch {
107                addr,
108                task,
109                goal,
110                policy,
111                idem,
112                session,
113                effort,
114                model,
115            },
116            Command::ConfigureBuilding {
117                addr,
118                sandbox,
119                mcp,
120                desktop,
121                context_second_threshold,
122                idem,
123            } => Self::ConfigureBuilding {
124                addr,
125                sandbox,
126                mcp,
127                desktop,
128                context_second_threshold,
129                idem,
130            },
131            Command::ProbeEndpoint {
132                name,
133                base_url,
134                dialect,
135                secret,
136                auth_header,
137                tuning,
138                idem,
139            } => Self::ProbeEndpoint {
140                name,
141                base_url,
142                dialect,
143                secret,
144                auth_header,
145                tuning,
146                idem,
147            },
148            Command::AttachEndpoint {
149                name,
150                base_url,
151                dialect,
152                secret,
153                auth_header,
154                admit,
155                tuning,
156                idem,
157            } => Self::AttachEndpoint {
158                name,
159                base_url,
160                dialect,
161                secret,
162                auth_header,
163                admit,
164                tuning,
165                idem,
166            },
167            Command::SelectModel {
168                endpoint,
169                model,
170                tag,
171                context_tokens,
172                max_output_tokens,
173                input,
174                idem,
175            } => Self::SelectModel {
176                endpoint,
177                model,
178                tag,
179                context_tokens,
180                max_output_tokens,
181                input,
182                idem,
183            },
184            Command::OpenSession {
185                addr,
186                carry,
187                from,
188                idem,
189            } => Self::OpenSession {
190                addr,
191                carry,
192                from,
193                idem,
194            },
195            Command::CreateBuilding {
196                addr,
197                template,
198                idem,
199            } => Self::CreateBuilding {
200                addr,
201                template,
202                idem,
203            },
204            Command::PutSecret { value, .. } => match value {},
205            Command::Steer { run, text, idem } => Self::Steer { run, text, idem },
206            Command::Cancel { run, idem } => Self::Cancel { run, idem },
207            Command::Halt { scope, idem } => Self::Halt { scope, idem },
208            Command::Reveal { at, idem } => Self::Reveal { at, idem },
209            Command::RestoreDiscard { restoration, idem } => {
210                Self::RestoreDiscard { restoration, idem }
211            }
212            Command::RemoveBuilding { addr, idem } => Self::RemoveBuilding { addr, idem },
213            Command::DoctorInstall { item, idem } => Self::DoctorInstall { item, idem },
214            Command::DoctorRefresh { idem } => Self::DoctorRefresh { idem },
215            Command::ConnectToolkit { toolkit, idem } => Self::ConnectToolkit { toolkit, idem },
216            Command::Release { scope, idem } => Self::Release { scope, idem },
217            Command::BatchByBuilding { addr, idem } => Self::BatchByBuilding { addr, idem },
218            Command::Approve {
219                item,
220                verdict,
221                idem,
222            } => Self::Approve {
223                item,
224                verdict,
225                idem,
226            },
227            Command::HandOff { item, to, idem } => Self::HandOff { item, to, idem },
228            Command::Pursue { addr, step, idem } => Self::Pursue { addr, step, idem },
229            Command::SetAutonomy {
230                scope,
231                autonomy,
232                idem,
233            } => Self::SetAutonomy {
234                scope,
235                autonomy,
236                idem,
237            },
238            Command::PutDocument {
239                which,
240                base,
241                body,
242                idem,
243            } => Self::PutDocument {
244                which,
245                base,
246                body,
247                idem,
248            },
249            Command::PutIdentity { card, base, idem } => Self::PutIdentity { card, base, idem },
250            Command::PutRules(write) => Self::PutRules(write),
251            Command::RestoreFile { at, point, idem } => Self::RestoreFile { at, point, idem },
252            Command::PutGuide { progress, idem } => Self::PutGuide { progress, idem },
253            Command::ConfigureCity(settings) => Self::ConfigureCity(settings),
254            Command::PutRange(write) => Self::PutRange(write),
255            Command::DecideProposals(decisions) => Self::DecideProposals(decisions),
256            Command::PutSpine {
257                building,
258                which,
259                base,
260                body,
261                idem,
262            } => Self::PutSpine {
263                building,
264                which,
265                base,
266                body,
267                idem,
268            },
269            Command::PutPreferences { patch, idem } => Self::PutPreferences { patch, idem },
270            Command::PutShelved {
271                shelf,
272                name,
273                text,
274                idem,
275            } => Self::PutShelved {
276                shelf,
277                name,
278                text,
279                idem,
280            },
281            Command::Auth { token } => Self::Auth { token },
282        }
283    }
284}
285
286#[cfg(test)]
287#[allow(
288    clippy::float_arithmetic,
289    clippy::unwrap_used,
290    clippy::expect_used,
291    clippy::panic,
292    clippy::indexing_slicing,
293    clippy::string_slice,
294    clippy::arithmetic_side_effects,
295    reason = "test code"
296)]
297mod tests {
298    use super::*;
299    /// The runtime half of "a credential cannot be enrolled over a
300    /// connection": the compile-time half is `NoSecret` having no values,
301    /// and this is what happens to bytes that try anyway.
302    #[test]
303    fn a_wire_frame_carrying_put_secret_fails_to_decode() {
304        let json = r#"{"put_secret":{"realm":"anthropic","name":"api","value":"sk-nope"}}"#;
305        let decoded: Result<WireCommand, _> = serde_json::from_str(json);
306        let err = decoded.expect_err("PutSecret has no wire form");
307        assert!(
308            !err.to_string().contains("sk-nope"),
309            "the refusal must not echo the bytes it protects"
310        );
311    }
312}