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::{
31    CitySettings, DoorAnswer, DoorOpening, DoorStep, PolicyChange, ProposalDecisions, RangeWrite,
32    RulesWrite, SessionNaming,
33};
34
35/// The Command set a socket can carry. `PutSecret` is unreachable because
36/// `NoSecret` has no values.
37pub type WireCommand = Command<NoSecret>;
38
39impl<Secret> Command<Secret> {
40    /// The deduplication key. `None` only for the two Commands that change
41    /// nothing: `Auth`, and `PutSecret` whose effect is confined to the host
42    /// process and whose replay writes the same Vault entry.
43    pub fn idem(&self) -> Option<&IdemKey> {
44        match *self {
45            Self::Dispatch { ref idem, .. }
46            | Self::Wake { ref idem, .. }
47            | Self::OpenSession { ref idem, .. }
48            | Self::ProbeEndpoint { ref idem, .. }
49            | Self::ConfigureBuilding { ref idem, .. }
50            | Self::CreateBuilding { ref idem, .. }
51            | Self::RemoveBuilding { ref idem, .. }
52            | Self::Steer { ref idem, .. }
53            | Self::Cancel { ref idem, .. }
54            | Self::Halt { ref idem, .. }
55            | Self::Release { ref idem, .. }
56            | Self::BatchByBuilding { ref idem, .. }
57            | Self::Approve { ref idem, .. }
58            | Self::HandOff { ref idem, .. }
59            | Self::SetAutonomy { ref idem, .. }
60            | Self::Pursue { ref idem, .. }
61            | Self::PutDocument { ref idem, .. }
62            | Self::PutIdentity { ref idem, .. }
63            | Self::PutRules(RulesWrite { ref idem, .. })
64            | Self::ConfigureCity(CitySettings { ref idem, .. })
65            | Self::RestoreFile { ref idem, .. }
66            | Self::PutGuide { ref idem, .. }
67            | Self::PutSpine { ref idem, .. }
68            | Self::PutRange(RangeWrite { ref idem, .. })
69            | Self::DecideProposals(ProposalDecisions { ref idem, .. })
70            | Self::PutPreferences { ref idem, .. }
71            | Self::PutShelved { ref idem, .. }
72            | Self::AttachEndpoint { ref idem, .. }
73            | Self::SelectModel { ref idem, .. }
74            | Self::Reveal { ref idem, .. }
75            | Self::RestoreDiscard { ref idem, .. }
76            | Self::DoctorInstall { ref idem, .. }
77            | Self::DoctorRefresh { ref idem, .. }
78            | Self::ConnectToolkit { ref idem, .. }
79            | Self::NameSession(SessionNaming { ref idem, .. })
80            | Self::ChangeRunPolicy(PolicyChange { ref idem, .. })
81            | Self::OpenRemoteDoor(DoorOpening { ref idem, .. })
82            | Self::ConfirmRemoteDoor(DoorAnswer { ref idem, .. })
83            | Self::ReplaceCityKey(DoorStep { ref idem })
84            | Self::CloseRemoteDoor(DoorStep { ref idem }) => Some(idem),
85            Self::PutSecret { .. } | Self::Auth { .. } => None,
86        }
87    }
88}
89
90impl From<WireCommand> for Command {
91    /// Widening a wire frame into the in-process Command set. Total: the
92    /// `PutSecret` arm is unreachable because its payload cannot exist.
93    fn from(wire: WireCommand) -> Self {
94        match wire {
95            // One line per variant keeps this table inside the function budget.
96            #[rustfmt::skip]
97            Command::Wake { source, subject, body, idem } => Self::Wake { source, subject, body, idem },
98            Command::Dispatch {
99                addr,
100                task,
101                goal,
102                policy,
103                idem,
104                session,
105                effort,
106                model,
107            } => Self::Dispatch {
108                addr,
109                task,
110                goal,
111                policy,
112                idem,
113                session,
114                effort,
115                model,
116            },
117            Command::ConfigureBuilding {
118                addr,
119                sandbox,
120                mcp,
121                desktop,
122                context_second_threshold,
123                idem,
124            } => Self::ConfigureBuilding {
125                addr,
126                sandbox,
127                mcp,
128                desktop,
129                context_second_threshold,
130                idem,
131            },
132            Command::ProbeEndpoint {
133                name,
134                base_url,
135                dialect,
136                secret,
137                auth_header,
138                tuning,
139                idem,
140            } => Self::ProbeEndpoint {
141                name,
142                base_url,
143                dialect,
144                secret,
145                auth_header,
146                tuning,
147                idem,
148            },
149            Command::AttachEndpoint {
150                name,
151                base_url,
152                dialect,
153                secret,
154                auth_header,
155                admit,
156                tuning,
157                idem,
158            } => Self::AttachEndpoint {
159                name,
160                base_url,
161                dialect,
162                secret,
163                auth_header,
164                admit,
165                tuning,
166                idem,
167            },
168            Command::SelectModel {
169                endpoint,
170                model,
171                tag,
172                context_tokens,
173                max_output_tokens,
174                input,
175                idem,
176            } => Self::SelectModel {
177                endpoint,
178                model,
179                tag,
180                context_tokens,
181                max_output_tokens,
182                input,
183                idem,
184            },
185            Command::OpenSession {
186                addr,
187                carry,
188                from,
189                idem,
190            } => Self::OpenSession {
191                addr,
192                carry,
193                from,
194                idem,
195            },
196            // One line per variant keeps this table inside the function budget.
197            #[rustfmt::skip]
198            Command::CreateBuilding { addr, template, idem } =>
199                Self::CreateBuilding { addr, template, idem },
200            Command::PutSecret { value, .. } => match value {},
201            Command::Steer { run, text, idem } => Self::Steer { run, text, idem },
202            Command::Cancel { run, idem } => Self::Cancel { run, idem },
203            Command::Halt { scope, idem } => Self::Halt { scope, idem },
204            Command::Reveal { at, idem } => Self::Reveal { at, idem },
205            Command::RestoreDiscard { restoration, idem } => {
206                Self::RestoreDiscard { restoration, idem }
207            }
208            Command::RemoveBuilding { addr, idem } => Self::RemoveBuilding { addr, idem },
209            Command::DoctorInstall { item, idem } => Self::DoctorInstall { item, idem },
210            Command::DoctorRefresh { idem } => Self::DoctorRefresh { idem },
211            Command::ConnectToolkit { toolkit, idem } => Self::ConnectToolkit { toolkit, idem },
212            Command::Release { scope, idem } => Self::Release { scope, idem },
213            Command::BatchByBuilding { addr, idem } => Self::BatchByBuilding { addr, idem },
214            Command::Approve {
215                item,
216                verdict,
217                idem,
218            } => Self::Approve {
219                item,
220                verdict,
221                idem,
222            },
223            Command::HandOff { item, to, idem } => Self::HandOff { item, to, idem },
224            Command::Pursue { addr, step, idem } => Self::Pursue { addr, step, idem },
225            Command::SetAutonomy {
226                scope,
227                autonomy,
228                idem,
229            } => Self::SetAutonomy {
230                scope,
231                autonomy,
232                idem,
233            },
234            Command::PutDocument {
235                which,
236                base,
237                body,
238                idem,
239            } => Self::PutDocument {
240                which,
241                base,
242                body,
243                idem,
244            },
245            Command::PutIdentity { card, base, idem } => Self::PutIdentity { card, base, idem },
246            Command::PutRules(write) => Self::PutRules(write),
247            Command::RestoreFile { at, point, idem } => Self::RestoreFile { at, point, idem },
248            Command::PutGuide { progress, idem } => Self::PutGuide { progress, idem },
249            Command::ConfigureCity(settings) => Self::ConfigureCity(settings),
250            Command::PutRange(write) => Self::PutRange(write),
251            Command::DecideProposals(decisions) => Self::DecideProposals(decisions),
252            Command::PutSpine {
253                building,
254                which,
255                base,
256                body,
257                idem,
258            } => Self::PutSpine {
259                building,
260                which,
261                base,
262                body,
263                idem,
264            },
265            Command::PutPreferences { patch, idem } => Self::PutPreferences { patch, idem },
266            Command::NameSession(naming) => Self::NameSession(naming),
267            Command::ChangeRunPolicy(change) => Self::ChangeRunPolicy(change),
268            Command::OpenRemoteDoor(opening) => Self::OpenRemoteDoor(opening),
269            Command::ReplaceCityKey(step) => Self::ReplaceCityKey(step),
270            Command::ConfirmRemoteDoor(answer) => Self::ConfirmRemoteDoor(answer),
271            Command::CloseRemoteDoor(step) => Self::CloseRemoteDoor(step),
272            Command::PutShelved {
273                shelf,
274                name,
275                text,
276                idem,
277            } => Self::PutShelved {
278                shelf,
279                name,
280                text,
281                idem,
282            },
283            Command::Auth { token } => Self::Auth { token },
284        }
285    }
286}
287
288#[cfg(test)]
289#[allow(
290    clippy::float_arithmetic,
291    clippy::unwrap_used,
292    clippy::expect_used,
293    clippy::panic,
294    clippy::indexing_slicing,
295    clippy::string_slice,
296    clippy::arithmetic_side_effects,
297    reason = "test code"
298)]
299mod tests {
300    use super::*;
301    /// The runtime half of "a credential cannot be enrolled over a
302    /// connection": the compile-time half is `NoSecret` having no values,
303    /// and this is what happens to bytes that try anyway.
304    #[test]
305    fn a_wire_frame_carrying_put_secret_fails_to_decode() {
306        let json = r#"{"put_secret":{"realm":"anthropic","name":"api","value":"sk-nope"}}"#;
307        let decoded: Result<WireCommand, _> = serde_json::from_str(json);
308        let err = decoded.expect_err("PutSecret has no wire form");
309        assert!(
310            !err.to_string().contains("sk-nope"),
311            "the refusal must not echo the bytes it protects"
312        );
313    }
314}