//! The claude login minter's requests: the subject swarm-controller sends an //! operator's login steps on, and what travels each way. //! //! Three crates name these: the controller sends the requests, the minter //! answers them, and the auth-callout responder grants the one subject to //! each. //! //! A login is two requests for one agent: //! [`Request::Start`](crate::claude_login::Request::Start) runs //! `claude auth login` and answers with the URL it printed, and //! [`Request::Code`](crate::claude_login::Request::Code) hands it the code the //! operator pasted and answers once the credentials are in the agent's store //! leaf. The running login lives in the minter's memory between the two, so //! the swarm runs one minter. use std::time::Duration; use serde::{Deserialize, Serialize}; /// The subject every request goes to. The controller is its one publisher /// and the minter its one subscriber; the minter answers on the request's /// reply subject. pub const SUBJECT: &str = "$SWARM.claude-login.request"; /// The longest the minter waits for `claude` to print the login URL, and /// then for it to finish once the code is in. pub const STEP_DEADLINE: Duration = Duration::from_mins(1); /// How long the controller waits for a reply, and the queue lets the minter /// answer: a step at its deadline, plus the store write. pub const REPLY_DEADLINE: Duration = Duration::from_secs(90); /// How long a started login waits for its code before the minter stops it /// and deletes its scratch directory. pub const SESSION_DEADLINE: Duration = Duration::from_mins(15); /// One step of an operator's login of one agent. /// /// No `Debug` derive: [`Request::Code`] carries the one-time code. #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "step", rename_all = "kebab-case")] pub enum Request { /// Start a login for `agent`, replacing one already running for it. Start { agent: String, /// The swarm UI user, as the gateway named them. operator: String, }, /// Hand the running login for `agent` the code the operator pasted. Code { agent: String, code: String }, /// Stop the running login for `agent`, if any. Cancel { agent: String }, } /// The minter's answer to one [`Request`]. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "state", rename_all = "kebab-case")] pub enum Reply { /// The login is running; the operator opens `url` and pastes the code it /// shows back. Started { url: String }, /// The credentials are stored in the agent's leaf, as its newest /// version. Stored, /// The running login, if there was one, is stopped. Cancelled, /// The step failed: why, with the last lines `claude` printed where it /// printed any. Failed { error: String }, } #[cfg(test)] mod tests { use super::*; #[test] fn the_wire_tags_the_request_by_step_and_the_reply_by_state() { let json = serde_json::to_value(Request::Code { agent: "atlas".to_owned(), code: "c".to_owned(), }) .expect("encodes"); assert_eq!(json["step"], "code"); assert_eq!(json["agent"], "atlas"); let json = serde_json::to_value(Reply::Stored).expect("encodes"); assert_eq!(json["state"], "stored"); } #[test] fn the_reply_deadline_outlasts_a_step() { assert!(REPLY_DEADLINE > STEP_DEADLINE); } }