swarm: serve an agent's icon at swarm scope
An agent is not fixed to a hive, so its icon cannot be resolved as hive -> agent. This adds the swarm-level half: an `agent-icons` KV bucket keyed by the agent name alone — no hive token, so an agent that moves hives keeps its icon and one that is stopped still has one — and `GET /api/agents/<name>/icon` on swarm-controller serving it same-origin, like every other `/api/*` route swarm-ui calls. 404 is the "this agent has no icon" answer, the same contract the per-agent harness's own `GET /icon` has for an unconfigured agent. Until the agent-side publisher lands, that is every agent's answer: the publisher runs inside the container and an agent's NATS grants are hive-scoped, which cannot authorise a write to a single-token agent key. The read side needs no grant change — the controller already holds `$KV.*.>` and `$JS.API.DIRECT.GET.*.>`. The response carries `Content-Security-Policy: sandbox` and `nosniff`: the body is an operator-authored SVG served from this daemon's own origin, and an SVG can carry script. Hive-side icon serving is untouched. Refs #4502
This commit is contained in:
parent
2e67656de8
commit
9513058a71
5 changed files with 317 additions and 1 deletions
91
swarm-queue-client/src/agent_icon.rs
Normal file
91
swarm-queue-client/src/agent_icon.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
//! 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, hyperhive#4502: *"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.
|
||||
//!
|
||||
//! ⚠️ **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.
|
||||
|
||||
#[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";
|
||||
|
||||
/// 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::BUCKET;
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn the_published_subject_carries_the_agent_and_no_placement() {
|
||||
assert_eq!(format!("$KV.{BUCKET}.iris"), "$KV.agent-icons.iris");
|
||||
}
|
||||
}
|
||||
|
|
@ -182,6 +182,12 @@ pub mod agent_status;
|
|||
/// formats it without linking the secret-store client.
|
||||
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.
|
||||
pub mod agent_icon;
|
||||
|
||||
/// The subject the swarm controller publishes on when the hive-wide knowledge
|
||||
/// repository has changed. One writer, many readers — every hive subscribes.
|
||||
///
|
||||
|
|
|
|||
Loading…
Reference in a new issue