Skip to main content

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