From c6b136468404691b1a2355004d98b3a29ac077f9 Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 18 Oct 2026 12:19:34 +0200 Subject: [PATCH] swarm-wire: take agent_icon's wire types --- hive-agent/src/swarm_agent_icon.rs | 2 +- swarm-controller/src/agent_icon.rs | 2 +- swarm-controller/src/routes/agents.rs | 2 +- swarm-nats-auth/src/policy.rs | 2 +- swarm-queue-client/src/agent_icon.rs | 72 ++--------------------- swarm-queue-client/src/agent_schedules.rs | 2 +- swarm-queue-client/src/lib.rs | 7 ++- swarm-wire/src/agent_icon.rs | 63 ++++++++++++++++++++ swarm-wire/src/lib.rs | 1 + 9 files changed, 77 insertions(+), 76 deletions(-) create mode 100644 swarm-wire/src/agent_icon.rs diff --git a/hive-agent/src/swarm_agent_icon.rs b/hive-agent/src/swarm_agent_icon.rs index a752fda8..3427fe10 100644 --- a/hive-agent/src/swarm_agent_icon.rs +++ b/hive-agent/src/swarm_agent_icon.rs @@ -72,7 +72,7 @@ pub fn spawn() { async fn run(agent: String) { let client = crate::swarm_queue::client().await; - let subject = swarm_queue_client::agent_icon::subject(&agent); + let subject = swarm_wire::agent_icon::subject(&agent); let icon = match read_icon(Path::new(ICON_PATH)) { Ok(icon) => icon, Err(e) => { diff --git a/swarm-controller/src/agent_icon.rs b/swarm-controller/src/agent_icon.rs index bf4f019d..ee871925 100644 --- a/swarm-controller/src/agent_icon.rs +++ b/swarm-controller/src/agent_icon.rs @@ -7,7 +7,7 @@ //! property of it, so there is nothing for a timestamp to mean here. //! //! **No hive is named on this path**, because the key does not carry one — -//! see `swarm_queue_client::agent_icon` for why, and for who writes it. +//! see `swarm_wire::agent_icon` for why, and for who writes it. //! //! This reader is also what creates the bucket: the agents that write it are //! granted their own key and nothing that could create a stream. diff --git a/swarm-controller/src/routes/agents.rs b/swarm-controller/src/routes/agents.rs index 7414b380..a348f437 100644 --- a/swarm-controller/src/routes/agents.rs +++ b/swarm-controller/src/routes/agents.rs @@ -1165,7 +1165,7 @@ pub(crate) async fn get_agent_icon( [ ( header::CONTENT_TYPE, - swarm_queue_client::agent_icon::MEDIA_TYPE.to_owned(), + swarm_wire::agent_icon::MEDIA_TYPE.to_owned(), ), ( header::CACHE_CONTROL, diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index 5eb39daf..0ba68a71 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -1964,7 +1964,7 @@ mod tests { /// A verified agent may write its own icon key and no other agent's. #[test] fn an_agent_may_write_only_its_own_icon_key() { - use swarm_queue_client::agent_icon::subject; + use swarm_wire::agent_icon::subject; let p = policy() .with_agent_token_subjects(vec!["$KV.agent-icons.{agent}".to_owned()]) diff --git a/swarm-queue-client/src/agent_icon.rs b/swarm-queue-client/src/agent_icon.rs index e6a9c163..98a65de8 100644 --- a/swarm-queue-client/src/agent_icon.rs +++ b/swarm-queue-client/src/agent_icon.rs @@ -1,56 +1,5 @@ -//! The per-agent icon KV bucket: one key per agent, holding the icon the -//! swarm shows for it. -//! -//! **Keyed by the agent alone — no hive token**, which is the one way this -//! bucket deliberately differs from its otherwise-identical sibling -//! `swarm_wire::agent_status` (`{hive}.{agent}`). An agent is not fixed to a -//! hive and can move between them; an icon keyed by where the agent -//! currently runs would be stranded under the old key by a migration, and -//! a reader would have to know the placement to ask the question at all. -//! Operator ruling: *"the bucket is per agent, not per hive. agents can -//! move hives."* -//! -//! That is also what makes the icon answerable for an agent whose -//! container is stopped: the value's lifetime is the agent's, not its -//! placement's. -//! -//! **Each agent writes its own key** with its own queue credential. The auth -//! callout grants that credential `$KV.agent-icons.` and no other key. -//! -//! The writer publishes to [`crate::agent_icon::subject`] directly instead of -//! opening the bucket, so that one subject is its whole grant. Creating the -//! bucket is left to the reader, with `open_or_create`. A write that arrives -//! before the bucket exists is refused with "no stream", and the writer -//! retries. - -#[cfg(feature = "kv")] -use crate::Error; - -/// The KV bucket agent icons are published into, keyed by agent name. -/// -/// A constant and not an option, for the reason -/// [`swarm_wire::agent_status::BUCKET`] gives: writer and reader must name the -/// same bucket, and an option is a way for the two to disagree. -pub const BUCKET: &str = "agent-icons"; - -/// The media type of every value in this bucket. -/// -/// The value is the icon's bytes **verbatim, not a JSON envelope**: an SVG -/// carries no metadata this bucket would have to describe, and the one -/// consumer serves the bytes straight back out. So the type is fixed here -/// rather than stored per entry — a publisher that has something other -/// than an SVG does not have an agent icon. -pub const MEDIA_TYPE: &str = "image/svg+xml"; - -/// The subject `agent`'s entry is written on: `$KV..`, where the -/// key is the agent name and nothing else. -/// -/// A KV put is a `JetStream` publish to this subject, so a writer that -/// publishes directly has to use the same subject the bucket's `get` reads. -#[must_use] -pub fn subject(agent: &str) -> String { - format!("$KV.{BUCKET}.{agent}") -} +//! The connection side of the per-agent icon KV bucket: opening it. The +//! bucket's name, media type and subject are `swarm_wire::agent_icon`. /// Open the agent-icon bucket, creating it if nothing has yet. /// @@ -60,26 +9,13 @@ pub fn subject(agent: &str) -> String { #[cfg(feature = "kv")] pub async fn open_or_create( client: &async_nats::Client, -) -> Result { +) -> Result { let js = async_nats::jetstream::new(client.clone()); crate::kv::open_or_create( &js, - BUCKET, + swarm_wire::agent_icon::BUCKET, "agent-icon", "Current icon published by each agent".to_owned(), ) .await } - -#[cfg(test)] -mod tests { - use super::subject; - - /// One token after the bucket, the agent name alone. That is what the - /// per-agent grant `$KV.agent-icons.{agent}` expands to, and what a - /// hive-scoped grant (`$KV...*`, two tokens) cannot match. - #[test] - fn the_published_subject_carries_the_agent_and_no_placement() { - assert_eq!(subject("iris"), "$KV.agent-icons.iris"); - } -} diff --git a/swarm-queue-client/src/agent_schedules.rs b/swarm-queue-client/src/agent_schedules.rs index 13537e8e..fcfdf9f0 100644 --- a/swarm-queue-client/src/agent_schedules.rs +++ b/swarm-queue-client/src/agent_schedules.rs @@ -17,7 +17,7 @@ //! agent's whole `JetStream` grant: `read_own` builds the stream handle //! without a `STREAM.INFO`. //! -//! Keyed by the agent alone, like [`crate::agent_icon`], and for the same +//! Keyed by the agent alone, like `swarm_wire::agent_icon`, and for the same //! reason: an agent can move between hives. /// The KV bucket schedules are published into, keyed by agent name. diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index e4afef89..e7b22d55 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -201,9 +201,10 @@ pub mod wanted; /// merge, are in `swarm_wire::agent_status`. pub mod agent_status; -/// The bucket agent icons are published into — one key per agent, with no -/// hive in it, unlike [`agent_status`]. See the module doc for why the -/// placement stays out of the key, and for who may write it. + +/// Opening the bucket agent icons are published into: one key per agent, with +/// no hive in it, unlike [`agent_status`]. The name, media type and subject, +/// and why the placement stays out of the key, are in `swarm_wire::agent_icon`. pub mod agent_icon; /// The bucket each agent mirrors its `AGENTS.md` into, and the edit command diff --git a/swarm-wire/src/agent_icon.rs b/swarm-wire/src/agent_icon.rs new file mode 100644 index 00000000..2f1703f5 --- /dev/null +++ b/swarm-wire/src/agent_icon.rs @@ -0,0 +1,63 @@ +//! The per-agent icon KV bucket: one key per agent, holding the icon the +//! swarm shows for it. +//! +//! **Keyed by the agent alone — no hive token**, which is the one way this +//! bucket deliberately differs from its otherwise-identical sibling +//! `swarm_wire::agent_status` (`{hive}.{agent}`). An agent is not fixed to a +//! hive and can move between them; an icon keyed by where the agent +//! currently runs would be stranded under the old key by a migration, and +//! a reader would have to know the placement to ask the question at all. +//! Operator ruling: *"the bucket is per agent, not per hive. agents can +//! move hives."* +//! +//! That is also what makes the icon answerable for an agent whose +//! container is stopped: the value's lifetime is the agent's, not its +//! placement's. +//! +//! **Each agent writes its own key** with its own queue credential. The auth +//! callout grants that credential `$KV.agent-icons.` and no other key. +//! +//! The writer publishes to [`subject`] directly instead of opening the +//! bucket, so that one subject is its whole grant. Creating the bucket is left +//! to the reader (`swarm_queue_client::agent_icon::open_or_create`). A write +//! that arrives before the bucket exists is refused with "no stream", and the +//! writer retries. + +/// The KV bucket agent icons are published into, keyed by agent name. +/// +/// A constant and not an option, for the reason +/// [`crate::agent_status::BUCKET`] gives: writer and reader must name the +/// same bucket, and an option is a way for the two to disagree. +pub const BUCKET: &str = "agent-icons"; + +/// The media type of every value in this bucket. +/// +/// The value is the icon's bytes **verbatim, not a JSON envelope**: an SVG +/// carries no metadata this bucket would have to describe, and the one +/// consumer serves the bytes straight back out. So the type is fixed here +/// rather than stored per entry — a publisher that has something other +/// than an SVG does not have an agent icon. +pub const MEDIA_TYPE: &str = "image/svg+xml"; + +/// The subject `agent`'s entry is written on: `$KV..`, where the +/// key is the agent name and nothing else. +/// +/// A KV put is a `JetStream` publish to this subject, so a writer that +/// publishes directly has to use the same subject the bucket's `get` reads. +#[must_use] +pub fn subject(agent: &str) -> String { + format!("$KV.{BUCKET}.{agent}") +} + +#[cfg(test)] +mod tests { + use super::subject; + + /// One token after the bucket, the agent name alone. That is what the + /// per-agent grant `$KV.agent-icons.{agent}` expands to, and what a + /// hive-scoped grant (`$KV...*`, two tokens) cannot match. + #[test] + fn the_published_subject_carries_the_agent_and_no_placement() { + assert_eq!(subject("iris"), "$KV.agent-icons.iris"); + } +} diff --git a/swarm-wire/src/lib.rs b/swarm-wire/src/lib.rs index c48a7429..0c6e9440 100644 --- a/swarm-wire/src/lib.rs +++ b/swarm-wire/src/lib.rs @@ -5,6 +5,7 @@ //! names a contract (the sender, the answerer, the auth-callout responder that //! grants the subject) depends on this one without linking either. +pub mod agent_icon; pub mod agent_inference; pub mod agent_login; pub mod agent_status;