Watch
0
0
Fork
You've already forked hyperhive
0

swarm-nats-auth: limit agent subscribe to its own inbox and input

An agent's queue credential could subscribe to every subject in the swarm
account, so any agent could read every other agent's terminal, turn state,
operator input and request replies. Agents are the least trusted clients
on the queue (mara's ruling on #4901), so their grants now carry a
subscribe allow-list:

- an agent's own credential (the harness and its subagent daemon):
  `_INBOX.agent.<agent>.>` and `$SWARM.input.<agent>`
- the hive's shared agent client: `_INBOX.client.<client-id>.>`

Hive and swarm-controller grants are unchanged (no `sub` claim, so still
every subject).

The clients connect with the matching inbox prefix, so the replies they
rely on still arrive: the icon write's JetStream ack and the schedule
DIRECT.GET. `connect_with_token` takes the agent name and sets the prefix
itself; the hive-shared fallback uses the new
`connect_with_inbox_prefix`.

The harness no longer subscribes to its input subject when it fell back
to the hive's shared client: that identity names no agent, so it cannot
be granted one agent's input without all of them. It logs that and
skips, the same as the schedule reader on that path.

Refs #4901
This commit is contained in:
atlas 2026-10-04 18:05:44 +02:00 • committed by mara
commit 52037133e2
8 changed files with 236 additions and 33 deletions

View file

@ -12,6 +12,10 @@
//! publish on the subject (`swarm-nats-auth`'s reader grant), and it names
//! the sender from the swarm gateway's authelia answer.
//!
//! **Own credential only.** The queue grants subscribe on this subject to the
//! agent's own credential alone; the hive's shared client names no agent, so
//! it cannot be granted one agent's input without every agent's.
//!
//! **Core NATS, no reply.** A message sent while this task is not subscribed
//! is gone; the controller sends it as a request so the operator gets a 503
//! for that instead. No agent grant covers a reply subject, so nothing here
@ -21,7 +25,7 @@ use futures_util::{Stream, StreamExt as _};
use hive_sh4re::inbox::DeliveredMessage;
use tokio::sync::mpsc::UnboundedSender;
use crate::swarm_queue::Connection;
use crate::swarm_queue::{Connection, Presented};
/// Start the subscriber, if this agent has a queue credential and a label to
/// name its subject with.
@ -38,9 +42,16 @@ pub fn spawn(tx: UnboundedSender<DeliveredMessage>) {
}
async fn run(tx: UnboundedSender<DeliveredMessage>, agent: String) {
let Some(Connection { client, .. }) = crate::swarm_queue::client().await else {
let Some(Connection { client, presented }) = crate::swarm_queue::client().await else {
return;
};
if presented != Presented::Agent {
tracing::warn!(
"connected with the hive's shared client, which may not subscribe to an \
agent's input subject; not taking input from the swarm queue"
);
return;
}
let subject = swarm_queue_client::agent_input_subject(&agent);
let subscriber = match client.subscribe(subject.clone()).await {
Ok(subscriber) => subscriber,

View file

@ -492,7 +492,8 @@ async fn connect_once() -> Option<Connection> {
}
}
let cfg = config()?;
match swarm_queue_client::connect(cfg.clone()).await {
let inbox = swarm_queue_client::client_inbox_prefix(&cfg.client_id);
match swarm_queue_client::connect_with_inbox_prefix(cfg.clone(), inbox).await {
Ok(client) => {
tracing::info!(
url = %cfg.url,
@ -534,7 +535,8 @@ async fn connect_as_agent<S: SecretSource>(
) -> anyhow::Result<async_nats::Client> {
secret.prime().await?;
let secret = Arc::new(secret);
swarm_queue_client::connect_with_token(url, ca_file, move || {
let agent = name.clone();
swarm_queue_client::connect_with_token(url, ca_file, &agent, move || {
let secret = Arc::clone(&secret);
let name = name.clone();
// Spawned so the future handed back is only a `JoinHandle`, which is