Watch
0
0
Fork
You've already forked hyperhive
0

swarm: let an agent publish its own icon

The auth callout grants an agent that presents its own queue credential
one more subject, `$KV.agent-icons.<agent>`: its own key in the
agent-icons bucket and no other. The hive's shared agent client is
granted none of the bucket, since every agent on a hive presents it.

hive-agent writes `/etc/hyperhive/icon.svg`, the file its `GET /icon`
serves, to that key once per start, as a JetStream publish straight to
the subject (what `kv::Store::put` sends, minus the bucket lookup), so
the one subject is the whole grant. No icon deletes the key. A failed
write, including one that arrives before the bucket exists, is retried
with backoff until acked. An agent connected with the hive's shared
client publishes nothing.

swarm-controller creates the bucket as soon as its queue connection is
up, instead of on the first icon read, so an agent's write does not
wait for someone to look.

Measured against a local nats-server with a user allowed publish on
`$KV.agent-icons.atlas` only: the write to its own key is stored and
readable, a write to `$KV.agent-icons.argus` is refused (the ack times
out), the DEL marker makes the key read as absent, and a write before
the bucket exists fails with "no responders".
This commit is contained in:
atlas 2026-09-28 10:55:45 +02:00 • committed by mara
commit e974194e3a
13 changed files with 348 additions and 27 deletions

View file

@ -14,14 +14,17 @@
//! container is stopped: the value's lifetime is the agent's, not its
//! placement's.
//!
//! ⚠️ **Nothing writes this bucket yet.** The publisher runs inside the
//! agent's own container, and an agent's NATS grants are hive-scoped
//! (`$KV.<bucket>.<hive>.*`), which cannot authorise a write to a
//! single-token agent key — so the publish side is blocked on a grant
//! shape the policy layer does not have today. Until it lands, every read
//! here answers "no icon", which is the same answer an agent that never
//! set one gets, and the same 404 the per-agent harness's own `GET /icon`
//! has always returned for an unconfigured agent.
//! **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 hive's shared agent client gets nothing in this bucket: every agent on
//! a hive presents that same client, so its grant cannot be narrowed to one
//! agent's 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;
@ -42,6 +45,16 @@ pub const BUCKET: &str = "agent-icons";
/// 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`]:
@ -77,15 +90,13 @@ pub async fn open_or_create(
#[cfg(test)]
mod tests {
use super::BUCKET;
use super::subject;
/// The published subject is `$KV.<bucket>.<key>`, and this key is the
/// agent name alone — so the subject carries **one** token after the
/// bucket. Pinned here because that is precisely what a hive-scoped
/// grant (`$KV.<bucket>.<hive>.*`, two tokens) cannot match, and the
/// reason the publish side needs a grant shape of its own.
/// 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!(format!("$KV.{BUCKET}.iris"), "$KV.agent-icons.iris");
assert_eq!(subject("iris"), "$KV.agent-icons.iris");
}
}

View file

@ -184,8 +184,7 @@ pub mod agent_token;
/// 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 what still has to land before
/// anything can write it.
/// placement stays out of the key, and for who may write it.
pub mod agent_icon;
/// The subject the swarm controller publishes on when the hive-wide knowledge