wire/command/kind.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//! Command kinds: names, steps, the wire enum.
7
8use kernel::model::{RunPolicy, Window};
9use kernel::{
10 Address, ApprovalId, Autonomy, Ceiling, DialectKind, Effort, GitOid, IdemKey, InputKinds,
11 McpServer, ModelTag, Origin, ResidentId, Restoration, Ruling, RunId, SandboxLimits, Sealed,
12 SessionName,
13};
14use serde::{Deserialize, Serialize};
15
16use crate::carried_name::{ProviderName, TemplateName, ToolkitSlug};
17use crate::command::shelf::Shelf;
18use crate::command::step::{
19 Carry, CitySettings, GovernedDocument, HaltScope, IdentityCard, ProposalDecisions, PursuitStep,
20 RangeWrite, RulesWrite, SpineDocument,
21};
22use crate::command::tuning::EndpointTuning;
23use crate::guide::GuideProgress;
24use crate::named_frames::named_frames;
25use crate::preference::PreferencePatch;
26
27named_frames! {
28/// Commands change state, require authorization, and are idempotent.
29///
30/// Deliberately *not* `#[non_exhaustive]`: the schema hash is this type's
31/// version mechanism, so the assembly layer must handle every one of them
32/// and a new one fails to compile until somebody decides what it does.
33/// That is the rule that keeps a button off the client until the city can
34/// answer the frame behind it.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "snake_case")]
37#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
38#[cfg_attr(feature = "schema", schemars(rename = "Command"))]
39pub enum Command<Secret = Sealed<String>> {
40 Dispatch {
41 addr: Address,
42 task: String,
43 goal: String,
44 /// The four values the run works under: mode, write limit,
45 /// admission requirement and landing policy, all four stated
46 /// (`crates/wire/spec/Command.lean` §8-57). Written as sent into the run's
47 /// `run_started` line.
48 policy: RunPolicy,
49 idem: IdemKey,
50 /// What this session is called, when it is a new one.
51 ///
52 /// `Some` means `addr` names a building and the city opens a
53 /// room of this name under it; `None` means `addr` already
54 /// names the room, which is how an earlier session is
55 /// continued. Two dispatches to one room are one session with a
56 /// history, and that is a different thing from two sessions
57 /// sharing a folder.
58 session: Option<SessionName>,
59 /// How hard the model is asked to think in this session.
60 ///
61 /// `None` leaves the layer above to answer, which is the city's
62 /// own configuration and, failing that, the provider's default.
63 /// A value is written into the session's own configuration when
64 /// its room is opened, so it is chosen once and holds for every
65 /// run in that room (`crates/city/Spec.lean` §8-14).
66 effort: Option<Effort>,
67 /// Which registered model this one dispatch calls, by its id.
68 ///
69 /// `None` takes the model behind the `main` tag. A value names a
70 /// model the city registered under some tag, so it arrives with
71 /// the endpoint and the window it was registered with; an id the
72 /// city never registered is refused before anything is written
73 /// (`crates/wire/spec/Command.lean` §8-48c).
74 model: Option<String>,
75 },
76 /// Asks a base URL what it serves, and attaches nothing.
77 ///
78 /// A person cannot choose from a list they have not seen, so the
79 /// list is asked for apart from attaching; the answer lands as
80 /// `endpoint_probed`, which the page folds like any other fact.
81 ///
82 /// It carries the same `tuning` the attachment will, because a
83 /// probe that reached a gateway without the header that gateway
84 /// requires answers 401 for a key that is in fact good.
85 ProbeEndpoint {
86 name: ProviderName,
87 base_url: String,
88 dialect: DialectKind,
89 secret: Option<String>,
90 auth_header: Option<String>,
91 tuning: EndpointTuning,
92 idem: IdemKey,
93 },
94 /// What a building's runs may reach: the sandbox's limits, the
95 /// external servers its tools come from, and the windows on this
96 /// person's own machine its desktop connector may touch.
97 ///
98 /// Each field is optional and an absent one leaves that section
99 /// alone; an empty `mcp` list is a building that reaches no server,
100 /// which is a different statement from not saying.
101 ///
102 /// `desktop` is the allowlist's text and not a parsed value: the
103 /// connector that reads that file at start-up is the authority on
104 /// its syntax and fails closed, and a second parser here would be a
105 /// second authority (`crates/city/Spec.lean` §8-26).
106 ConfigureBuilding {
107 addr: Address,
108 sandbox: Option<SandboxLimits>,
109 mcp: Option<Vec<McpServer>>,
110 desktop: Option<String>,
111 /// Where the context reminder's second rung sits, as one layer
112 /// states it: a raw percent on the wire. The domain is
113 /// `kernel::config::SecondThreshold`'s one construction point,
114 /// and a deserializer that enforced it here would be the second
115 /// place that rule lives.
116 context_second_threshold: Option<u64>,
117 idem: IdemKey,
118 },
119 AttachEndpoint {
120 name: ProviderName,
121 /// The base URL as a provider's documentation prints it; the
122 /// dialect owns the path that hangs off it.
123 base_url: String,
124 dialect: DialectKind,
125 /// `secret:<realm>/<name>`, or absent for a local server that
126 /// asks for no credential.
127 secret: Option<String>,
128 /// Header name for providers that do not take a bearer token.
129 auth_header: Option<String>,
130 /// Which of the models this endpoint serves the city admits. An
131 /// empty list admits everything it serves, which is what a
132 /// person who did not look at the list meant.
133 admit: Vec<String>,
134 /// What this endpoint is called, how long it may take, and what
135 /// every request to it carries, kept beside the registration so a
136 /// call made a week later is made the way the person set it up.
137 tuning: EndpointTuning,
138 idem: IdemKey,
139 },
140 /// Point one tag at one model of an attached endpoint, with the two
141 /// facts no model list returns.
142 SelectModel {
143 endpoint: ProviderName,
144 model: String,
145 tag: ModelTag,
146 /// The model's window. Absent when nobody stated one, and the
147 /// context reminder then stays silent rather than measuring a
148 /// conversation against a number nobody gave it.
149 context_tokens: Option<Window>,
150 /// The model's output ceiling. Absent when the person did not
151 /// state one: the city then takes the catalogue's figure, and
152 /// where the catalogue has no row the model is registered
153 /// without a ceiling rather than with a zero one.
154 max_output_tokens: Option<Ceiling>,
155 /// What the person says the model accepts. Absent, the catalogue and
156 /// the preset table answer (`crates/gateway/Spec.lean` §8-37, D16).
157 input: Option<InputKinds>,
158 idem: IdemKey,
159 },
160 /// Starts a new session at an address: same room, a fresh stretch of conversation in it, so a
161 /// person who changed the model starts it on purpose rather than having the room's frozen shape
162 /// move behind their back (`crates/sprawling/Spec.lean` §8-82). [`Carry`] names what crosses; the default
163 /// keeps nothing. Writes `session_opened`.
164 OpenSession {
165 addr: Address,
166 carry: Carry,
167 /// What this session continues, when it continues something: a
168 /// branch is a session whose first run begins from a line of
169 /// another conversation, and everything else about it - model,
170 /// effort, handoff - is what [`Carry`] and the room already say
171 /// (`crates/sprawling/Spec.lean` §8-82).
172 from: Option<Origin>,
173 idem: IdemKey,
174 },
175 CreateBuilding {
176 addr: Address,
177 template: TemplateName,
178 idem: IdemKey,
179 },
180 /// Take a building out of the city. Its files move under the
181 /// reserved subtree and its history stays in the Ledger, so nothing
182 /// the person made is lost; a building with a run going is refused.
183 ///
184 /// Writes `building_removed`.
185 RemoveBuilding {
186 addr: Address,
187 idem: IdemKey,
188 },
189 /// The one Command with no byte form. `Secret` is `Sealed<String>` in
190 /// process and uninhabited on the wire.
191 PutSecret {
192 realm: String,
193 name: String,
194 value: Secret,
195 },
196 Steer {
197 run: RunId,
198 text: String,
199 idem: IdemKey,
200 },
201 Cancel {
202 run: RunId,
203 idem: IdemKey,
204 },
205 Halt {
206 scope: HaltScope,
207 idem: IdemKey,
208 },
209 /// Show a person this address in their own file manager. The
210 /// grammar is the guard: an `Address` cannot climb out of the city,
211 /// so there is no way to spell a request for anything else.
212 Reveal {
213 at: Address,
214 idem: IdemKey,
215 },
216 /// Put one recycle-bin row back. The frame carries the row's own
217 /// restoration rather than a path, because a path can be discarded
218 /// twice and the row already knows which commit holds the bytes.
219 RestoreDiscard {
220 restoration: Restoration,
221 idem: IdemKey,
222 },
223 /// Install one thing this machine lacks, named as the doctor's
224 /// answer names it.
225 ///
226 /// Only a recipe this city may run is run. A recipe that has to be
227 /// printed, and one that has no command at all, are refused with
228 /// what the person does instead: a script piped into a shell is
229 /// code nobody read, and that rule does not soften because the
230 /// request arrived from a page rather than from a terminal.
231 DoctorInstall {
232 item: String,
233 idem: IdemKey,
234 },
235 /// Look at this machine again, in place of the snapshot taken when
236 /// the city was served.
237 ///
238 /// [`Query::Doctor`](crate::Query::Doctor) answers that snapshot,
239 /// which a page that has just installed something must not be given.
240 /// Probing starts programs for seconds, so it runs here, where the
241 /// city already serialises work, rather than inside a read.
242 DoctorRefresh {
243 idem: IdemKey,
244 },
245 Release {
246 scope: HaltScope,
247 idem: IdemKey,
248 },
249 BatchByBuilding {
250 addr: Address,
251 idem: IdemKey,
252 },
253 Approve {
254 item: ApprovalId,
255 verdict: Ruling,
256 idem: IdemKey,
257 },
258 /// Give one waiting question to a resident to answer.
259 ///
260 /// A person who does not want to answer a question may name
261 /// somebody who will. This moves one item and leaves
262 /// [`Command::SetAutonomy`] to say who answers everything, because
263 /// handing over one question and appointing a standing delegate are
264 /// different decisions with different reach.
265 ///
266 /// Writes `question_handed`, after which the named resident may
267 /// answer this item even though it is not the standing delegate.
268 HandOff {
269 item: ApprovalId,
270 to: ResidentId,
271 idem: IdemKey,
272 },
273 SetAutonomy {
274 scope: HaltScope,
275 autonomy: Autonomy,
276 idem: IdemKey,
277 },
278 /// A goal the city keeps working towards until the work runs out.
279 ///
280 /// One frame with four steps rather than four frames: a person who can set a goal can pause it,
281 /// and splitting that into separate commands would let a client offer one without the other.
282 Pursue {
283 addr: Address,
284 step: PursuitStep,
285 idem: IdemKey,
286 },
287 /// Something happened outside. The city never asks whether anything
288 /// did; the service holding the connection pushes, and this is the
289 /// shape a push takes once it is inside.
290 ///
291 /// It carries no address. Where an arrival lands is the watch
292 /// table's answer and then triage's, so a caller that could name a
293 /// room would be a caller that could reach past the routing a person
294 /// wrote.
295 Wake {
296 source: String,
297 subject: String,
298 body: String,
299 idem: IdemKey,
300 },
301 /// Writes one of the three documents that govern this city, whole,
302 /// only if it still holds `base`: the raw editor and the identity
303 /// cards are two writers of one file (`crates/wire/spec/Answer/Identity.lean` §8-59).
304 PutDocument {
305 which: GovernedDocument,
306 base: String,
307 body: String,
308 idem: IdemKey,
309 },
310 /// One identity card: the city rewrites the card's keys in `base`'s
311 /// identity area and leaves every other byte (`crates/wire/spec/Answer/Identity.lean` §8-59).
312 PutIdentity {
313 card: IdentityCard,
314 base: String,
315 idem: IdemKey,
316 },
317 /// Writes a building's `RULES.toml` whole (`crates/wire/spec/Command/Step.lean` §8-60).
318 PutRules(RulesWrite),
319 /// Takes one file of the city's own tree back to what a checkpoint
320 /// holds, or away when it holds none (`crates/wire/spec/Answer/Automation.lean` §8-62).
321 RestoreFile {
322 at: Address,
323 point: GitOid,
324 idem: IdemKey,
325 },
326 /// Writes this city's first-run guide progress whole; the later of
327 /// two writes stays (`crates/wire/spec/Guide.lean` §8-68).
328 PutGuide {
329 progress: GuideProgress,
330 idem: IdemKey,
331 },
332 /// Writes the city's own layer (`crates/wire/spec/Command/Step.lean` §8-61).
333 ConfigureCity(CitySettings),
334 /// Writes one of a building's own spine documents.
335 ///
336 /// The body replaces the file whole. Unlike [`Command::PutDocument`]
337 /// these have a second writer - a resident reaches `Roadmap.md`
338 /// through `plan` and the others through `edit` - so `base` carries
339 /// the text the sender started from and a file that moved underneath
340 /// it is refused rather than overwritten. One guard for two writers,
341 /// and the same one `edit` holds resident-side.
342 PutSpine {
343 building: Address,
344 which: SpineDocument,
345 base: String,
346 body: String,
347 idem: IdemKey,
348 },
349 /// Saves edits on one document version (`crates/wire/spec/Command/Step.lean` §8-72).
350 PutRange(RangeWrite),
351 /// Decides proposal cards on one document (`crates/wire/spec/Answer/Proposals.lean` §8-73).
352 DecideProposals(ProposalDecisions),
353 /// Connects one outside application through the broker that holds its OAuth, answered with the
354 /// whole shelf: the broker was asked, so every row is fresh, and the pressed one carries its
355 /// consent page.
356 ///
357 /// **The client opens the consent page, never the city**, which may run on a machine nobody is
358 /// looking at. The `IdemKey` stops a second press from opening a second account on one
359 /// application.
360 ConnectToolkit {
361 toolkit: ToolkitSlug,
362 idem: IdemKey,
363 },
364 /// Change one fact about how this person reads their own city.
365 ///
366 /// One named fact per frame rather than a whole record: two screens settling different things
367 /// must not be able to overwrite each other's field on the way past.
368 PutPreferences {
369 patch: PreferencePatch,
370 idem: IdemKey,
371 },
372 /// Write one skill or script onto a shelf.
373 ///
374 /// The shelf and the name are separate because the city owns where a shelf lives: the two
375 /// shelves sit inside the reserved subtree, which no write domain reaches, so this is the door
376 /// a person writes there through and there is no path to spell. `name` may carry
377 /// sub-directories, which is how a script keeps its folder.
378 ///
379 /// The body replaces the file whole, for the reason [`Command::PutDocument`] gives. Writes
380 /// `shelved_document_written`.
381 PutShelved {
382 shelf: Shelf,
383 name: String,
384 text: String,
385 idem: IdemKey,
386 },
387 /// Presenting a pairing token. Read-only, hence no `IdemKey`; the token is plain here because a
388 /// token that must cross a wire has, by definition, no secrecy left to protect in transit - it
389 /// is sealed the moment it lands (see `server::decide_handshake`).
390 Auth {
391 token: String,
392 },
393}
394
395/// The Command surface, in declaration order — the order the handshake
396/// hash mixes these names in.
397pub const COMMAND_NAMES;
398}