diff --git a/AGENTS.md b/AGENTS.md index 496242a9..e6b9f51d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -138,6 +138,9 @@ Swarm-level: with **no identity** and a TLS failure that names no cause. - **`swarm-queue-client/`** — the one authenticated connect to the swarm NATS queue. +- **`swarm-wire/`** — serde-only request and reply contracts services + exchange over the queue (builder, claude login), so naming one links + neither the queue client nor the store client. - **`swarm-nats-auth/`** — NATS auth-callout responder; without it the queue is fail-closed and admits nobody. - **`swarm-agent-verify/`** — checks a `swarm-agent..` diff --git a/Cargo.lock b/Cargo.lock index 8ecce245..b6674478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4811,6 +4811,7 @@ dependencies = [ "serde_json", "swarm-queue-client", "swarm-secret-client", + "swarm-wire", "tokio", "tracing", ] @@ -4846,6 +4847,7 @@ dependencies = [ "serde_json", "swarm-queue-client", "swarm-secret-client", + "swarm-wire", "tokio", "tracing", ] @@ -4889,6 +4891,7 @@ dependencies = [ "swarm-matrix-client", "swarm-queue-client", "swarm-secret-client", + "swarm-wire", "tempfile", "tokio", "tower", @@ -5010,6 +5013,7 @@ dependencies = [ "swarm-agent-verify", "swarm-queue-client", "swarm-secret-client", + "swarm-wire", "tokio", "tracing", "tracing-subscriber", @@ -5076,6 +5080,14 @@ dependencies = [ "vaultrs", ] +[[package]] +name = "swarm-wire" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "swarmctl" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ed69dd19..a1f718b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "swarm-logs-auth", "swarm-metrics-auth", "swarm-secret-client", + "swarm-wire", "swarmctl", ] @@ -118,6 +119,7 @@ swarm-controller-api = { path = "swarm-controller-api" } swarm-matrix-client = { path = "swarm-matrix-client" } swarm-queue-client = { path = "swarm-queue-client" } swarm-secret-client = { path = "swarm-secret-client" } +swarm-wire = { path = "swarm-wire" } thiserror = "2" vaultrs = "0.8" # vaultrs's transport crates. Direct dependencies because one endpoint is diff --git a/swarm-builder/Cargo.toml b/swarm-builder/Cargo.toml index 4275fee9..bbb5d424 100644 --- a/swarm-builder/Cargo.toml +++ b/swarm-builder/Cargo.toml @@ -15,10 +15,11 @@ async-nats.workspace = true futures-util.workspace = true hive-log.workspace = true serde_json.workspace = true -# The build subject and the shapes the controller sends and reads back. swarm-queue-client.workspace = true # The forge token and queue client secret paths, and the store login. swarm-secret-client.workspace = true +# The build subject and the shapes the controller sends and reads back. +swarm-wire.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/swarm-builder/src/archetypes.rs b/swarm-builder/src/archetypes.rs index 76222ade..cf35081f 100644 --- a/swarm-builder/src/archetypes.rs +++ b/swarm-builder/src/archetypes.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use std::process::Stdio; use std::time::Duration; -use swarm_queue_client::builder::ArchetypesReply; +use swarm_wire::builder::ArchetypesReply; /// What every evaluation is run with, fixed at start. pub struct Evaluator { @@ -105,7 +105,7 @@ mod tests { nix: PathBuf::from("nix"), hyperhive: "/nix/store/h-source".to_owned(), nixpkgs: "/nix/store/n-source".to_owned(), - deadline: swarm_queue_client::builder::ARCHETYPES_DEADLINE, + deadline: swarm_wire::builder::ARCHETYPES_DEADLINE, } } diff --git a/swarm-builder/src/main.rs b/swarm-builder/src/main.rs index bf0cfe2b..5c835f37 100644 --- a/swarm-builder/src/main.rs +++ b/swarm-builder/src/main.rs @@ -1,11 +1,11 @@ //! The swarm builder's build service. //! //! Connects to the swarm queue as its own OIDC client, takes build requests -//! from swarm-controller on [`swarm_queue_client::builder::SUBJECT`], builds +//! from swarm-controller on [`swarm_wire::builder::SUBJECT`], builds //! each agent config at its commit, answers on the request's reply subject, //! and logs one line per build. It writes nothing to the forge: the //! controller turns the reply into a commit status. It also answers -//! [`swarm_queue_client::builder::ARCHETYPES_SUBJECT`] with the agent +//! [`swarm_wire::builder::ARCHETYPES_SUBJECT`] with the agent //! archetypes its hyperhive source declares, which the controller stores. //! //! The store identity comes from the `BAO_*` environment. The queue client @@ -19,13 +19,13 @@ use std::time::Duration; use anyhow::{Context, Result, anyhow, bail}; use futures_util::StreamExt as _; -use swarm_queue_client::builder::{ - ARCHETYPES_DEADLINE, ARCHETYPES_SUBJECT, BUILD_DEADLINE, BuildAgent, QUEUE_GROUP, - REPLY_DEADLINE, Reply, SUBJECT, -}; use swarm_queue_client::{ClientSecret, QueueConfig}; use swarm_secret_client::forge::Credential; use swarm_secret_client::stored_secret::StoredSecret; +use swarm_wire::builder::{ + ARCHETYPES_DEADLINE, ARCHETYPES_SUBJECT, BUILD_DEADLINE, BuildAgent, QUEUE_GROUP, + REPLY_DEADLINE, Reply, SUBJECT, +}; mod archetypes; mod nix_build; @@ -168,10 +168,10 @@ async fn answer_evaluations( }; let reply = evaluator.eval().await; match &reply { - swarm_queue_client::builder::ArchetypesReply::Evaluated { archetypes, .. } => { + swarm_wire::builder::ArchetypesReply::Evaluated { archetypes, .. } => { tracing::info!(count = archetypes.len(), "archetypes evaluated"); } - swarm_queue_client::builder::ArchetypesReply::Failed { error_tail } => { + swarm_wire::builder::ArchetypesReply::Failed { error_tail } => { tracing::warn!(%error_tail, "archetype evaluation failed"); } } diff --git a/swarm-builder/src/nix_build.rs b/swarm-builder/src/nix_build.rs index 099a28de..b6937378 100644 --- a/swarm-builder/src/nix_build.rs +++ b/swarm-builder/src/nix_build.rs @@ -16,7 +16,7 @@ use std::path::PathBuf; use std::process::Stdio; use std::time::Duration; -use swarm_queue_client::builder::{BuildAgent, Reply}; +use swarm_wire::builder::{BuildAgent, Reply}; use tokio::io::AsyncReadExt as _; /// The most of `nix`'s stderr a failed build's reply carries, from its end. @@ -293,7 +293,7 @@ pub(crate) fn tail(stderr: &str, status: std::process::ExitStatus) -> String { #[cfg(test)] mod tests { use super::*; - use swarm_queue_client::builder::BUILD_DEADLINE; + use swarm_wire::builder::BUILD_DEADLINE; fn job() -> BuildAgent { BuildAgent { diff --git a/swarm-claude-login/Cargo.toml b/swarm-claude-login/Cargo.toml index 68238712..b1af0ca1 100644 --- a/swarm-claude-login/Cargo.toml +++ b/swarm-claude-login/Cargo.toml @@ -21,6 +21,8 @@ hive-types.workspace = true swarm-queue-client.workspace = true # The credentials and queue client secret paths, and the store login. swarm-secret-client.workspace = true +# The login subject and the requests and replies it carries. +swarm-wire.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/swarm-claude-login/src/login.rs b/swarm-claude-login/src/login.rs index 17246354..1b504623 100644 --- a/swarm-claude-login/src/login.rs +++ b/swarm-claude-login/src/login.rs @@ -14,8 +14,8 @@ use std::process::Stdio; use std::sync::Arc; use std::time::Duration; -use swarm_queue_client::claude_login::Reply; use swarm_secret_client::claude_login::Credentials; +use swarm_wire::claude_login::Reply; use tokio::io::{AsyncBufReadExt as _, AsyncRead, AsyncWriteExt as _, BufReader}; use tokio::process::{Child, ChildStdin, Command}; use tokio::sync::{Mutex, watch}; diff --git a/swarm-claude-login/src/main.rs b/swarm-claude-login/src/main.rs index 36bcdf3f..1efcf0b0 100644 --- a/swarm-claude-login/src/main.rs +++ b/swarm-claude-login/src/main.rs @@ -2,7 +2,7 @@ //! //! Connects to the swarm queue as its own OIDC client, takes an operator's //! login steps from swarm-controller on -//! [`swarm_queue_client::claude_login::SUBJECT`], runs `claude auth login` +//! [`swarm_wire::claude_login::SUBJECT`], runs `claude auth login` //! for the agent named ([`login`]), and writes the credentials it produces to //! that agent's store leaf, a new version over any stored before. The agent's //! harness takes a version newer than the one it last installed. @@ -16,10 +16,10 @@ use std::sync::Arc; use anyhow::{Context, Result, bail}; use futures_util::StreamExt as _; -use swarm_queue_client::claude_login::{Reply, Request, SESSION_DEADLINE, STEP_DEADLINE, SUBJECT}; use swarm_queue_client::{ClientSecret, QueueConfig}; use swarm_secret_client::claude_login::{Credentials, client_secret_path, credentials_path}; use swarm_secret_client::stored_secret::StoredSecret; +use swarm_wire::claude_login::{Reply, Request, SESSION_DEADLINE, STEP_DEADLINE, SUBJECT}; mod login; @@ -190,7 +190,7 @@ mod tests { use std::sync::Arc; use std::time::Duration; - use swarm_queue_client::claude_login::{Reply, Request}; + use swarm_wire::claude_login::{Reply, Request}; use super::{handle, login}; diff --git a/swarm-controller/Cargo.toml b/swarm-controller/Cargo.toml index f1e40ade..14204773 100644 --- a/swarm-controller/Cargo.toml +++ b/swarm-controller/Cargo.toml @@ -118,6 +118,8 @@ swarm-queue-client = { workspace = true, features = [ # name and the object's shape are agreements between the two ends, and a # second spelling here would be a store this hive could not read. swarm-secret-client.workspace = true +# The builder and claude login subjects, requests and replies it sends. +swarm-wire.workspace = true # The appservice calls `matrix_account::agent_token` mints agents' accounts # with — shared with `swarm-matrix-ctl`, which pins the same device id. swarm-matrix-client.workspace = true diff --git a/swarm-controller/src/archetypes.rs b/swarm-controller/src/archetypes.rs index 1507e57e..6c755a9f 100644 --- a/swarm-controller/src/archetypes.rs +++ b/swarm-controller/src/archetypes.rs @@ -16,7 +16,7 @@ use hive_sh4re::permissions::Permission; use problem_details::ProblemDetails; use serde::{Deserialize, Serialize}; use swarm_queue_client::archetypes::{ArchetypeMeta, BUILTIN_SOURCE, SourceRecord}; -use swarm_queue_client::builder::{ +use swarm_wire::builder::{ ARCHETYPES_DEADLINE, ARCHETYPES_SUBJECT, ArchetypesReply, EvalArchetypes, }; use utoipa::ToSchema; @@ -361,7 +361,7 @@ mod tests { let mut requests = builder .queue_subscribe( ARCHETYPES_SUBJECT, - swarm_queue_client::builder::QUEUE_GROUP.to_owned(), + swarm_wire::builder::QUEUE_GROUP.to_owned(), ) .await .expect("subscribe"); diff --git a/swarm-controller/src/builder.rs b/swarm-controller/src/builder.rs index cef00653..88323bca 100644 --- a/swarm-controller/src/builder.rs +++ b/swarm-controller/src/builder.rs @@ -16,7 +16,7 @@ use std::future::Future; use forgejo_api::structs::CommitStatusState; use hive_jobq::scheduler::Outcome; -use swarm_queue_client::builder::{BuildAgent, REPLY_DEADLINE, Reply, SUBJECT}; +use swarm_wire::builder::{BuildAgent, REPLY_DEADLINE, Reply, SUBJECT}; use crate::agent_renewal::unix_now; use crate::forge::{self, CONFIG_ORG}; @@ -534,7 +534,7 @@ mod tests { let builder = nats.client().await; let mut requests = builder - .queue_subscribe(SUBJECT, swarm_queue_client::builder::QUEUE_GROUP.to_owned()) + .queue_subscribe(SUBJECT, swarm_wire::builder::QUEUE_GROUP.to_owned()) .await .expect("subscribe"); // A message the builder sends itself and receives back proves the diff --git a/swarm-controller/src/claude_login.rs b/swarm-controller/src/claude_login.rs index 10425785..857a9c58 100644 --- a/swarm-controller/src/claude_login.rs +++ b/swarm-controller/src/claude_login.rs @@ -21,7 +21,7 @@ use axum::extract::{Path, State}; use axum::http::{HeaderMap, StatusCode}; use problem_details::ProblemDetails; use serde::{Deserialize, Serialize}; -use swarm_queue_client::claude_login::{REPLY_DEADLINE, Reply, Request, SUBJECT}; +use swarm_wire::claude_login::{REPLY_DEADLINE, Reply, Request, SUBJECT}; use utoipa::ToSchema; use crate::{AppState, error_problem}; @@ -243,7 +243,7 @@ mod tests { use std::sync::Mutex; use axum::http::StatusCode; - use swarm_queue_client::claude_login::{Reply, Request}; + use swarm_wire::claude_login::{Reply, Request}; use super::{LoginSink, relay}; diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index ce88efd3..ed5499a3 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -601,7 +601,7 @@ async fn build_agent( rev: String, pr: u64, ) -> hive_jobq::scheduler::Outcome { - let job = swarm_queue_client::builder::BuildAgent { agent, rev, pr }; + let job = swarm_wire::builder::BuildAgent { agent, rev, pr }; let builds = builder::runs::Builds::Live(deps.queue.clone()); let link_base = std::env::var(PUBLIC_URL_ENV).ok(); builder::run( diff --git a/swarm-nats-auth/Cargo.toml b/swarm-nats-auth/Cargo.toml index 7ba2da59..0bf8e78b 100644 --- a/swarm-nats-auth/Cargo.toml +++ b/swarm-nats-auth/Cargo.toml @@ -33,6 +33,8 @@ swarm-queue-client = { workspace = true, features = ["notices"] } # credentials `swarm-agent-verify` checks a presented token against. swarm-secret-client.workspace = true swarm-agent-verify.workspace = true +# The builder and login subjects it grants. +swarm-wire.workspace = true tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index c18b1153..088be44b 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -281,11 +281,11 @@ impl Policy { Permissions { publish: vec![Self::inbox(client_id)], subscribe: Some(vec![ - swarm_queue_client::builder::SUBJECT.to_owned(), - swarm_queue_client::builder::ARCHETYPES_SUBJECT.to_owned(), + swarm_wire::builder::SUBJECT.to_owned(), + swarm_wire::builder::ARCHETYPES_SUBJECT.to_owned(), Self::inbox(client_id), ]), - responses: Some(swarm_queue_client::builder::REPLY_DEADLINE), + responses: Some(swarm_wire::builder::REPLY_DEADLINE), } } @@ -296,10 +296,10 @@ impl Policy { Permissions { publish: vec![Self::inbox(client_id)], subscribe: Some(vec![ - swarm_queue_client::claude_login::SUBJECT.to_owned(), + swarm_wire::claude_login::SUBJECT.to_owned(), Self::inbox(client_id), ]), - responses: Some(swarm_queue_client::claude_login::REPLY_DEADLINE), + responses: Some(swarm_wire::claude_login::REPLY_DEADLINE), } } @@ -661,11 +661,11 @@ impl Policy { // back on this client's inbox, which it already subscribes to. swarm_queue_client::review_classifier::SUBJECT.to_owned(), // A build of an agent config, sent as a request the same way. - swarm_queue_client::builder::SUBJECT.to_owned(), + swarm_wire::builder::SUBJECT.to_owned(), // An archetype evaluation, sent as a request the same way. - swarm_queue_client::builder::ARCHETYPES_SUBJECT.to_owned(), + swarm_wire::builder::ARCHETYPES_SUBJECT.to_owned(), // An operator's claude login step, sent as a request the same way. - swarm_queue_client::claude_login::SUBJECT.to_owned(), + swarm_wire::claude_login::SUBJECT.to_owned(), // The wanted-state buckets — one per hive, all created and written // by this single client. // @@ -1532,8 +1532,8 @@ mod tests { #[test] fn the_builder_hears_its_subject_and_inbox_only_and_answers_by_reply() { - use swarm_queue_client::builder::{ARCHETYPES_SUBJECT, REPLY_DEADLINE, SUBJECT}; use swarm_queue_client::client_inbox_prefix; + use swarm_wire::builder::{ARCHETYPES_SUBJECT, REPLY_DEADLINE, SUBJECT}; let p = policy() .with_builders(vec![BUILDER.to_owned()]) @@ -1562,7 +1562,7 @@ mod tests { #[test] fn only_the_reader_may_send_a_build() { - use swarm_queue_client::builder::{ARCHETYPES_SUBJECT, SUBJECT}; + use swarm_wire::builder::{ARCHETYPES_SUBJECT, SUBJECT}; let p = with_classifier().with_builders(vec![BUILDER.to_owned()]); let reader = p.permissions("swarm-controller").expect("the reader"); @@ -1589,8 +1589,8 @@ mod tests { #[test] fn the_claude_login_minter_hears_its_subject_and_inbox_only_and_answers_by_reply() { - use swarm_queue_client::claude_login::{REPLY_DEADLINE, SUBJECT}; use swarm_queue_client::client_inbox_prefix; + use swarm_wire::claude_login::{REPLY_DEADLINE, SUBJECT}; let p = policy() .with_claude_logins(vec![CLAUDE_LOGIN.to_owned()]) @@ -1602,7 +1602,7 @@ mod tests { for refused in [ ">".to_owned(), "$SWARM.>".to_owned(), - swarm_queue_client::builder::SUBJECT.to_owned(), + swarm_wire::builder::SUBJECT.to_owned(), reply(&client_inbox_prefix("swarm-controller")), ] { assert!( @@ -1618,7 +1618,7 @@ mod tests { #[test] fn only_the_reader_may_send_a_login_step() { - use swarm_queue_client::claude_login::SUBJECT; + use swarm_wire::claude_login::SUBJECT; let p = with_classifier() .with_builders(vec![BUILDER.to_owned()]) diff --git a/swarm-queue-client/src/archetypes.rs b/swarm-queue-client/src/archetypes.rs index 519a9a4e..e99984a5 100644 --- a/swarm-queue-client/src/archetypes.rs +++ b/swarm-queue-client/src/archetypes.rs @@ -4,7 +4,7 @@ //! //! **swarm-controller is the only writer and the only reader.** The swarm //! builder evaluates the source and answers on -//! [`crate::builder::ARCHETYPES_SUBJECT`]; the controller checks each +//! `swarm_wire::builder::ARCHETYPES_SUBJECT`; the controller checks each //! archetype's settings the way `POST /api/agents` checks them and writes the //! result here, so the create screen lists archetypes from a value that a //! create would accept. diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index ee0ef4ea..534e933e 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -494,13 +494,6 @@ pub mod review_classifier; mod startup_read; pub use startup_read::read_with_startup_retry; -/// The builder's build subject and the request and reply it carries. -pub mod builder; - -/// The claude login minter's subject and the requests and replies it -/// carries. -pub mod claude_login; - /// Only the fields this needs; authelia returns several. #[derive(serde::Deserialize)] struct TokenResponse { diff --git a/swarm-wire/Cargo.toml b/swarm-wire/Cargo.toml new file mode 100644 index 00000000..ee968020 --- /dev/null +++ b/swarm-wire/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "swarm-wire" +version.workspace = true +edition.workspace = true + +[dependencies] +serde.workspace = true +serde_json.workspace = true + +[lints] +workspace = true diff --git a/swarm-queue-client/src/builder.rs b/swarm-wire/src/builder.rs similarity index 56% rename from swarm-queue-client/src/builder.rs rename to swarm-wire/src/builder.rs index 9ba1e6bd..aeb66f54 100644 --- a/swarm-queue-client/src/builder.rs +++ b/swarm-wire/src/builder.rs @@ -82,40 +82,97 @@ pub enum ArchetypesReply { #[cfg(test)] mod tests { + use serde_json::json; + use super::*; #[test] - fn the_wire_tags_the_reply_by_state() { - let json = serde_json::to_value(Reply::Built { - out_path: "/nix/store/x".to_owned(), - }) - .expect("encodes"); - assert_eq!(json["state"], "built"); - assert_eq!(json["out_path"], "/nix/store/x"); - let json = serde_json::to_value(Reply::Failed { - error_tail: "e".to_owned(), - }) - .expect("encodes"); - assert_eq!(json["state"], "failed"); + fn the_build_request_serializes_as_pinned() { + let req = BuildAgent { + agent: "atlas".to_owned(), + rev: "abc".to_owned(), + pr: 7, + }; + let value = serde_json::to_value(&req).expect("encodes"); + assert_eq!(value, json!({"agent": "atlas", "rev": "abc", "pr": 7})); + assert_eq!( + serde_json::from_value::(value).expect("decodes"), + req + ); + } + + #[test] + fn the_reply_serializes_as_pinned() { + for (reply, wire) in [ + ( + Reply::Built { + out_path: "/nix/store/x".to_owned(), + }, + json!({"state": "built", "out_path": "/nix/store/x"}), + ), + ( + Reply::Failed { + error_tail: "e".to_owned(), + }, + json!({"state": "failed", "error_tail": "e"}), + ), + ] { + assert_eq!(serde_json::to_value(&reply).expect("encodes"), wire); + assert_eq!( + serde_json::from_value::(wire).expect("decodes"), + reply + ); + } + } + + #[test] + fn the_archetypes_request_and_reply_serialize_as_pinned() { + let req = EvalArchetypes::default(); + let value = serde_json::to_value(&req).expect("encodes"); + assert_eq!(value, json!({})); + assert_eq!( + serde_json::from_value::(value).expect("decodes"), + req + ); + + let mut archetypes = serde_json::Map::new(); + archetypes.insert("coder".to_owned(), json!({"description": "d"})); + for (reply, wire) in [ + ( + ArchetypesReply::Evaluated { + source: "/nix/store/h".to_owned(), + archetypes, + }, + json!({ + "state": "evaluated", + "source": "/nix/store/h", + "archetypes": {"coder": {"description": "d"}}, + }), + ), + ( + ArchetypesReply::Failed { + error_tail: "e".to_owned(), + }, + json!({"state": "failed", "error_tail": "e"}), + ), + ] { + assert_eq!(serde_json::to_value(&reply).expect("encodes"), wire); + assert_eq!( + serde_json::from_value::(wire).expect("decodes"), + reply + ); + } + } + + #[test] + fn the_subjects_are_pinned() { + assert_eq!(SUBJECT, "$SWARM.builder.build"); + assert_eq!(ARCHETYPES_SUBJECT, "$SWARM.builder.archetypes"); + assert_eq!(QUEUE_GROUP, "builder"); } #[test] fn the_reply_deadline_outlasts_the_build() { assert!(REPLY_DEADLINE > BUILD_DEADLINE); } - - #[test] - fn the_archetypes_reply_is_tagged_by_state() { - let json = serde_json::to_value(ArchetypesReply::Evaluated { - source: "/nix/store/h".to_owned(), - archetypes: serde_json::Map::new(), - }) - .expect("encodes"); - assert_eq!(json["state"], "evaluated"); - let json = serde_json::to_value(ArchetypesReply::Failed { - error_tail: "e".to_owned(), - }) - .expect("encodes"); - assert_eq!(json["state"], "failed"); - } } diff --git a/swarm-queue-client/src/claude_login.rs b/swarm-wire/src/claude_login.rs similarity index 61% rename from swarm-queue-client/src/claude_login.rs rename to swarm-wire/src/claude_login.rs index d1242cec..d541bea2 100644 --- a/swarm-queue-client/src/claude_login.rs +++ b/swarm-wire/src/claude_login.rs @@ -71,19 +71,68 @@ pub enum Reply { #[cfg(test)] mod tests { + use serde_json::json; + 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"); + fn the_request_serializes_as_pinned() { + for (req, wire) in [ + ( + Request::Start { + agent: "atlas".to_owned(), + operator: "mara".to_owned(), + }, + json!({"step": "start", "agent": "atlas", "operator": "mara"}), + ), + ( + Request::Code { + agent: "atlas".to_owned(), + code: "c".to_owned(), + }, + json!({"step": "code", "agent": "atlas", "code": "c"}), + ), + ( + Request::Cancel { + agent: "atlas".to_owned(), + }, + json!({"step": "cancel", "agent": "atlas"}), + ), + ] { + assert_eq!(serde_json::to_value(&req).expect("encodes"), wire); + assert!(serde_json::from_value::(wire).expect("decodes") == req); + } + } + + #[test] + fn the_reply_serializes_as_pinned() { + for (reply, wire) in [ + ( + Reply::Started { + url: "https://u".to_owned(), + }, + json!({"state": "started", "url": "https://u"}), + ), + (Reply::Stored, json!({"state": "stored"})), + (Reply::Cancelled, json!({"state": "cancelled"})), + ( + Reply::Failed { + error: "e".to_owned(), + }, + json!({"state": "failed", "error": "e"}), + ), + ] { + assert_eq!(serde_json::to_value(&reply).expect("encodes"), wire); + assert_eq!( + serde_json::from_value::(wire).expect("decodes"), + reply + ); + } + } + + #[test] + fn the_subject_is_pinned() { + assert_eq!(SUBJECT, "$SWARM.claude-login.request"); } #[test] diff --git a/swarm-wire/src/lib.rs b/swarm-wire/src/lib.rs new file mode 100644 index 00000000..10c4c562 --- /dev/null +++ b/swarm-wire/src/lib.rs @@ -0,0 +1,9 @@ +//! The request and reply shapes swarm services exchange over the queue: the +//! subjects, their deadlines and the serde types that travel each way. +//! +//! Serde only, with no queue client and no secret-store client. A crate that +//! names a contract (the sender, the answerer, the auth-callout responder that +//! grants the subject) depends on this one without linking either. + +pub mod builder; +pub mod claude_login;