Docs (review on #4948): the harness's restart settings are not useful to a reader, so the `Restart`/`RestartSec` bullet in docs/turn-loop and the "30 seconds later, no start limit" wording in docs/swarm are removed. What stays is the behaviour: the harness exits and comes back until the queue accepts it. Grant: with the harness's shared-client fallback gone, nothing presents the hive's shared agent client (`hive-<hive>-agent`) to the queue; swarm-logs uses that OIDC client over HTTP only. swarm-nats-auth now refuses it outright instead of granting the configured `$SWARM.term.{hive}.>` / `$SWARM.agent-state.{hive}.>` publish and its `_INBOX.client.<id>.>` subscribe, so an agent holding it can no longer publish as a sibling. The agent arm still runs before the hive arm, so that id never falls through to its hive's grant. Removed with it: the `--agent-publish-subject` flag and its two uses in swarm-nats.nix, `Policy`'s agent subject list, and swarm-queue-client's `client_inbox_prefix`. The module-eval now asserts the flag is absent; the policy tests assert the refusal and that the hive itself is still admitted.
99 lines
3.9 KiB
Rust
99 lines
3.9 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());
|
|
match js.get_key_value(BUCKET).await {
|
|
Ok(store) => Ok(store),
|
|
Err(e) => {
|
|
tracing::info!(
|
|
bucket = BUCKET,
|
|
reason = %e,
|
|
"agent-icon bucket not available, creating it"
|
|
);
|
|
js.create_key_value(async_nats::jetstream::kv::Config {
|
|
bucket: BUCKET.to_owned(),
|
|
description: "Current icon published by each agent".to_owned(),
|
|
history: 1,
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.map_err(|source| Error::CreateBucket {
|
|
bucket: BUCKET.to_owned(),
|
|
source,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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");
|
|
}
|
|
}
|