Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-queue-client/src/agent_icon.rs
atlas b874d21cda hive-c0re/swarm-*: drop clippy allows and dedupe unix_now + KV open_or_create
No behaviour change. Casts replaced by try_from / Duration math; schedules.rs
reuses agent_renewal::unix_now; the five swarm-queue-client KV open-or-create
copies share one private kv::open_or_create.
2026-10-09 10:25:48 +02:00

85 lines
3.4 KiB
Rust

//! 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
//! [`crate::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.<agent>` 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
/// [`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.<bucket>.<key>`, 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}")
}
/// Open the agent-icon bucket, creating it if nothing has yet.
///
/// `history: 1`, same rationale as [`crate::agent_status::open_or_create`]:
/// a consumer wants each agent's current icon, not every icon it has ever
/// had.
#[cfg(feature = "kv")]
pub async fn open_or_create(
client: &async_nats::Client,
) -> Result<async_nats::jetstream::kv::Store, Error> {
let js = async_nats::jetstream::new(client.clone());
crate::kv::open_or_create(
&js,
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.<bucket>.<hive>.*`, two tokens) cannot match.
#[test]
fn the_published_subject_carries_the_agent_and_no_placement() {
assert_eq!(subject("iris"), "$KV.agent-icons.iris");
}
}