hive-agent: present this agent's own queue credential, then fall back
When the per-agent secret `queue-identity.nix` fetched is present, the harness connects with `swarm-agent.<agent>.<secret>` as a static token and publishes on `$SWARM.term.<agent>` and `$SWARM.agent-state.<agent>`. When it is absent, or that first connect fails for any reason, a refusal from a responder that does not verify agent tokens included, it connects with the hive's shared OIDC client and publishes on the hive-scoped subjects as before. Which one it took is logged once per connect. `swarm_queue_client::connect_with_token` is the static-token connect: no retry on the initial attempt, so the caller sees the refusal and can fall back. Reconnects share the existing backoff, now a named function. Closes #4630
This commit is contained in:
parent
0c1fb44a4f
commit
727065c960
5 changed files with 287 additions and 76 deletions
|
|
@ -27,6 +27,7 @@
|
|||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::events::BusEvent;
|
||||
use crate::swarm_queue::{Connection, Presented};
|
||||
use crate::term_msg::{ClassifyCtx, TermMsg, classify};
|
||||
|
||||
/// Subject family carrying agent terminal rows, the swarm-wide agreement this
|
||||
|
|
@ -112,37 +113,50 @@ fn serialized_len(msg: &TermMsg) -> Option<usize> {
|
|||
serde_json::to_vec(msg).ok().map(|v| v.len())
|
||||
}
|
||||
|
||||
/// Start the publish task, if this agent has both queue coordinates and an
|
||||
/// identity the subject can be derived from.
|
||||
/// The subject this agent's rows go to under the credential it connected
|
||||
/// with, or `None` when a hive client id names no hive.
|
||||
fn subject(presented: &Presented, agent: &str) -> Option<String> {
|
||||
match presented {
|
||||
Presented::Agent => Some(format!("{SUBJECT_PREFIX}.{agent}")),
|
||||
Presented::Hive { client_id } => {
|
||||
let Some(hive) = hive_from_client_id(client_id) else {
|
||||
tracing::warn!(
|
||||
%client_id,
|
||||
expected = format!("{CLIENT_ID_PREFIX}<hive>{CLIENT_ID_SUFFIX}"),
|
||||
"queue client id does not name a hive; not publishing the terminal upward"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
Some(format!("{SUBJECT_PREFIX}.{hive}.{agent}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start the publish task, if this agent has a queue credential and a label
|
||||
/// to name its subject with.
|
||||
///
|
||||
/// Returns without spawning in every other case — no queue, an unparseable
|
||||
/// client id, no label — each of which is a legal state for an agent rather
|
||||
/// than an error, and each logged once here rather than per row.
|
||||
/// Returns without spawning in every other case — no queue or no label — each
|
||||
/// of which is a legal state for an agent rather than an error, and each
|
||||
/// logged once here rather than per row.
|
||||
pub fn spawn(rx: broadcast::Receiver<BusEvent>) {
|
||||
let Some(cfg) = crate::swarm_queue::config() else {
|
||||
if !crate::swarm_queue::configured() {
|
||||
// `swarm_queue::init` already said why at boot; repeating it here
|
||||
// would be the same fact logged twice.
|
||||
return;
|
||||
};
|
||||
let Some(hive) = hive_from_client_id(&cfg.client_id) else {
|
||||
tracing::warn!(
|
||||
client_id = %cfg.client_id,
|
||||
expected = format!("{CLIENT_ID_PREFIX}<hive>{CLIENT_ID_SUFFIX}"),
|
||||
"queue client id does not name a hive; not publishing the terminal upward"
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
let agent = crate::identity::label();
|
||||
if agent.is_empty() {
|
||||
tracing::warn!("this agent has no label; not publishing the terminal upward");
|
||||
return;
|
||||
}
|
||||
let subject = format!("{SUBJECT_PREFIX}.{hive}.{agent}");
|
||||
tokio::spawn(run(rx, subject));
|
||||
tokio::spawn(run(rx, agent));
|
||||
}
|
||||
|
||||
async fn run(mut rx: broadcast::Receiver<BusEvent>, subject: String) {
|
||||
let Some(client) = crate::swarm_queue::client().await else {
|
||||
async fn run(mut rx: broadcast::Receiver<BusEvent>, agent: String) {
|
||||
let Some(Connection { client, presented }) = crate::swarm_queue::client().await else {
|
||||
return;
|
||||
};
|
||||
let Some(subject) = subject(&presented, &agent) else {
|
||||
return;
|
||||
};
|
||||
tracing::info!(subject, "publishing the agent terminal to the swarm queue");
|
||||
|
|
@ -203,7 +217,7 @@ async fn publish(client: &async_nats::Client, subject: &str, msg: TermMsg) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DROPPED_BODY, fit, hive_from_client_id};
|
||||
use super::{DROPPED_BODY, Presented, fit, hive_from_client_id, subject};
|
||||
use crate::events::LiveEvent;
|
||||
use crate::term_msg::{BodyFormat, ClassifyCtx, Level, TermMsg, classify};
|
||||
|
||||
|
|
@ -243,6 +257,27 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Each credential publishes on the subject it is granted: its own under
|
||||
/// the agent's credential, its hive's under the shared one.
|
||||
#[test]
|
||||
fn the_subject_follows_the_credential_the_agent_connected_with() {
|
||||
assert_eq!(
|
||||
subject(&Presented::Agent, "mara").as_deref(),
|
||||
Some("$SWARM.term.mara")
|
||||
);
|
||||
let hive = Presented::Hive {
|
||||
client_id: "hive-alpha-agent".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
subject(&hive, "mara").as_deref(),
|
||||
Some("$SWARM.term.alpha.mara")
|
||||
);
|
||||
let unparseable = Presented::Hive {
|
||||
client_id: "hive-alpha".to_owned(),
|
||||
};
|
||||
assert_eq!(subject(&unparseable, "mara"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_row_that_already_fits_is_published_unchanged() {
|
||||
let msg = TermMsg::new(Level::Info, "turn ok")
|
||||
|
|
|
|||
Loading…
Reference in a new issue