Skip to main content

wire/answer/
toolkits.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//! Which outside applications the broker offers, and where each one
7//! stands for this city.
8//!
9//! This is a reading of right now and it is never folded from the
10//! Ledger, on the same grounds as `McpHealth` (`crates/wire/spec/Answer/McpHealth.lean`
11//! ยง8-34): whether an account is connected this minute is a fact about
12//! this minute, and a recorded copy would still call it connected an
13//! hour after the person revoked it.
14//!
15//! One shape answers both the question and the act. Asking what is on
16//! the shelf and asking to connect one of its rows come back as the
17//! same list, so a client draws one rendering rather than two that have
18//! to agree.
19
20use kernel::AxError;
21use serde::{Deserialize, Serialize};
22
23use crate::carried_name::ToolkitSlug;
24
25/// The shelf, or the reason there is no shelf to show.
26///
27/// Exhaustive rather than a list that is empty in three different
28/// senses: "you have not given this city a key yet", "the broker is
29/// unreachable" and "the broker has nothing for you" are three
30/// different things for a person to do next, and an empty `Vec` says
31/// all three at once.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
35pub enum ToolkitsAnswer {
36    /// No project key has been enrolled, so there is nobody to ask. The
37    /// page shows the key field, and this is not a failure: it is the
38    /// first step, and drawing it as an error would tell a new person
39    /// that something broke.
40    Unenrolled,
41    /// What the broker published, in the order it published it.
42    ///
43    /// The broker's order rather than a best-first one: somebody
44    /// scanning this list twice finds the same application in the same
45    /// place.
46    Shelf { toolkits: Vec<ToolkitLine> },
47    /// The broker was asked and did not answer. The whole refusal
48    /// travels, so the page draws the city's wording rather than
49    /// inventing a second one.
50    Refused { refusal: Box<AxError> },
51}
52
53/// One application on the shelf.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
56pub struct ToolkitLine {
57    pub slug: ToolkitSlug,
58    /// What the broker calls it in prose, which is what a person
59    /// recognises it by.
60    pub name: String,
61    /// How this application authenticates, in the broker's own word -
62    /// `oauth2`, `api_key`, `bearer_token`. Carried rather than reduced
63    /// to a flag: the word decides what the person is about to be
64    /// shown, and this city is not the authority on which schemes
65    /// exist.
66    pub auth: String,
67    pub standing: Standing,
68}
69
70/// Where one application stands for this city.
71///
72/// Four states, each a different next action: connect it, finish the
73/// consent page that is already open, nothing, or read why the last
74/// attempt failed. Every one of them is the broker's own reading rather
75/// than something this city remembered - the page a person left open
76/// yesterday is not evidence that they finished with it.
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
80pub enum Standing {
81    /// Nothing here has connected it.
82    Absent,
83    /// A consent page exists and the person has not finished with it.
84    ///
85    /// The url travels so the client can open it, and keeps travelling
86    /// so the client can offer it again: a blocked popup and a closed
87    /// tab both leave a person who needs this link a second time, and a
88    /// url held only at the moment the command answered would be gone.
89    Awaiting { consent_url: String },
90    /// Connected, under the account name the broker returned. The name
91    /// is shown because one person can hold two accounts on the same
92    /// application and "connected" alone would not say which.
93    Connected { alias: String },
94    /// The last attempt failed and the broker still says so.
95    Refused { refusal: Box<AxError> },
96}