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
|
|
@ -35,6 +35,7 @@ use tokio::sync::broadcast;
|
|||
use swarm_queue_client::wanted::AgentState;
|
||||
|
||||
use crate::events::{Bus, BusEvent, LiveEvent, TurnState};
|
||||
use crate::swarm_queue::{Connection, Presented};
|
||||
use crate::term_msg::iso8601_utc;
|
||||
|
||||
/// Subject family carrying agent turn-state headers, the swarm-wide
|
||||
|
|
@ -171,33 +172,43 @@ fn snapshot(bus: &Bus) -> AgentStateMsg {
|
|||
}
|
||||
}
|
||||
|
||||
/// 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 header goes 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 turn state 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 transition.
|
||||
/// 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 transition.
|
||||
pub fn spawn(bus: &Bus) {
|
||||
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 turn state upward"
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
let agent = crate::identity::label();
|
||||
if agent.is_empty() {
|
||||
tracing::warn!("this agent has no label; not publishing turn state upward");
|
||||
return;
|
||||
}
|
||||
let subject = format!("{SUBJECT_PREFIX}.{hive}.{agent}");
|
||||
tokio::spawn(run(bus.subscribe(), bus.clone(), subject));
|
||||
tokio::spawn(run(bus.subscribe(), bus.clone(), agent));
|
||||
}
|
||||
|
||||
/// Watch the bus and publish whenever the header actually changed.
|
||||
|
|
@ -218,8 +229,11 @@ pub fn spawn(bus: &Bus) {
|
|||
/// and it carries nothing this header reads. Every other variant, including
|
||||
/// any added later, funnels into the comparison and costs nothing when it
|
||||
/// changes nothing.
|
||||
async fn run(mut rx: broadcast::Receiver<BusEvent>, bus: Bus, subject: String) {
|
||||
let Some(client) = crate::swarm_queue::client().await else {
|
||||
async fn run(mut rx: broadcast::Receiver<BusEvent>, bus: Bus, 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 agent turn state to the swarm queue");
|
||||
|
|
@ -307,7 +321,7 @@ async fn publish(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AgentStateMsg, SUBJECT_PREFIX, hive_from_client_id};
|
||||
use super::{AgentStateMsg, Presented, hive_from_client_id, subject};
|
||||
use crate::events::TurnState;
|
||||
use swarm_queue_client::wanted::AgentState;
|
||||
|
||||
|
|
@ -393,10 +407,22 @@ mod tests {
|
|||
/// this pins the half that lives here.
|
||||
#[test]
|
||||
fn the_subject_is_the_prefix_then_the_hive_then_the_agent() {
|
||||
let hive = hive_from_client_id("hive-alpha-agent").expect("names a hive");
|
||||
let presented = Presented::Hive {
|
||||
client_id: "hive-alpha-agent".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
format!("{SUBJECT_PREFIX}.{hive}.mara"),
|
||||
"$SWARM.agent-state.alpha.mara"
|
||||
subject(&presented, "mara").as_deref(),
|
||||
Some("$SWARM.agent-state.alpha.mara")
|
||||
);
|
||||
}
|
||||
|
||||
/// An agent that connected with its own credential publishes on the
|
||||
/// subject that credential is granted, which names no hive.
|
||||
#[test]
|
||||
fn an_agent_on_its_own_credential_publishes_on_its_hive_free_subject() {
|
||||
assert_eq!(
|
||||
subject(&Presented::Agent, "mara").as_deref(),
|
||||
Some("$SWARM.agent-state.mara")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue