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