Watch
0
0
Fork
You've already forked hyperhive
0

hive-agent: a logged-in agent polls the swarm login, and logs out when it is deleted

wait_for_login read the store leaf only while the agent waited for a login.
A logged-in agent now reads it every 5 minutes, and a paused one once before
it resumes, so a login made or deleted in the swarm UI reaches an agent that
never failed. A newer version is installed; a store that answers nothing
stored after a version was installed means the stored login was deleted, and
the agent is logged out as /api/logout does it, with the recorded version
forgotten so the next login installs. A read that fails changes nothing.
This commit is contained in:
iris 2026-11-09 22:08:05 +01:00 • committed by mara
commit a2c9e9a8d4

View file

@ -1,6 +1,7 @@
//! The claude credentials the swarm's login minter stored for this agent, at
//! `swarm/agents/<agent>/claude-credentials`, read under the agent's own
//! certificate while it waits for a login ([`crate::login::wait_for_login`]).
//! certificate while it waits for a login ([`crate::login::wait_for_login`]),
//! and every few minutes while it runs ([`crate::login_watch`]).
//!
//! ⚠️ **Installed only when the store's version is newer than the last one
//! installed here**, never because the bytes differ. `claude` rotates the
@ -9,6 +10,10 @@
//! The last installed version is kept in [`VERSION_FILE`] under the harness
//! dir. A credentials file `wait_for_login` sees land resumes the agent as a
//! web-UI login does.
//!
//! A store that answers "nothing stored" after a version was installed here
//! means the operator deleted the stored login ([`Outcome::Deleted`]); a store
//! that does not answer is an error and means nothing.
use std::future::Future;
use std::path::{Path, PathBuf};
@ -43,8 +48,12 @@ pub fn is_newer(stored: u64, installed: Option<u64>) -> bool {
/// What one [`Pickup::check`] did.
#[derive(Debug, PartialEq, Eq)]
pub enum Outcome {
/// Nothing is stored for this agent.
/// Nothing is stored for this agent, and no version was ever installed
/// here: no swarm login has reached this agent.
NothingStored,
/// The store answered that nothing is stored, though version `installed`
/// was installed here: the operator deleted the stored login.
Deleted { installed: u64 },
/// The stored version is not newer than the installed one.
NotNewer { stored: u64, installed: u64 },
/// The stored version is installed now.
@ -120,7 +129,10 @@ impl<S: Source> Pickup<S> {
/// The store read failed, or writing the credentials or the version did.
pub async fn check(&self) -> anyhow::Result<Outcome> {
let Some((credentials, stored)) = self.source.fetch().await? else {
return Ok(Outcome::NothingStored);
return Ok(match read_version(&self.version_file)? {
Some(installed) => Outcome::Deleted { installed },
None => Outcome::NothingStored,
});
};
let installed = read_version(&self.version_file)?;
if !is_newer(stored, installed) {
@ -138,6 +150,19 @@ impl<S: Source> Pickup<S> {
.context("recording the installed credentials version")?;
Ok(Outcome::Installed { version: stored })
}
/// Forget the installed version, once the deletion it showed has been
/// acted on, so the next login installs whatever version it gets.
///
/// # Errors
/// Removing the version file failed for a reason other than its absence.
pub fn forget_installed(&self) -> anyhow::Result<()> {
match std::fs::remove_file(&self.version_file) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e).with_context(|| format!("removing {}", self.version_file.display())),
}
}
}
/// The version recorded in `file`, or `None` when there is no file.
@ -302,4 +327,50 @@ mod tests {
assert_eq!(installed(&d), None);
assert_eq!(read_version(&d.0.join(VERSION_FILE)).expect("read"), None);
}
#[tokio::test]
async fn a_login_deleted_after_an_install_is_reported_and_forgotten_on_request() {
let d = dir("deleted");
let p = pickup(&d, Some(("{\"v\":1}", 1)));
p.check().await.expect("install");
*p.source.0.lock().expect("lock") = None;
assert_eq!(
p.check().await.expect("check"),
Outcome::Deleted { installed: 1 }
);
// Reporting it changes nothing: the caller acts, then forgets.
assert_eq!(
p.check().await.expect("check again"),
Outcome::Deleted { installed: 1 }
);
p.forget_installed().expect("forget");
p.forget_installed().expect("forgetting twice is fine");
assert_eq!(p.check().await.expect("check"), Outcome::NothingStored);
*p.source.0.lock().expect("lock") = Some(("{\"v\":1}".to_owned(), 1));
assert_eq!(
p.check().await.expect("a new login"),
Outcome::Installed { version: 1 },
"the counter restarts after a delete, and its first login still installs"
);
}
struct Failing;
impl Source for Failing {
async fn fetch(&self) -> anyhow::Result<Option<(Credentials, u64)>> {
anyhow::bail!("the store did not answer")
}
}
#[tokio::test]
async fn a_failed_read_is_an_error_never_a_deletion() {
let d = dir("failing");
std::fs::write(d.0.join(VERSION_FILE), "4").expect("an installed version");
let p = Pickup::new(Failing, d.0.join("claude"), d.0.join(VERSION_FILE));
assert!(p.check().await.is_err());
assert_eq!(
read_version(&d.0.join(VERSION_FILE)).expect("read"),
Some(4)
);
}
}