agents: pull the forge token from bao; drop tea-login
forge-token.nix fetches swarm/agents/<agent>/forge-token under the agent's own store identity into /run/hive-agent-forge-token/token, and re-fetches on a timer so a rotation lands. hive-forge, the git credential helper, hive-forge-notify, forge-avatar-sync and the web UI read that file first and fall back to <state>/forge-token. tea-login is deleted: it copied the token into ~/.config/tea, which docs/swarm/credentials.md forbids for a store secret. hive-forge covers the same verbs. swarmctl gains agent mint-forge-token. Refs #3782
This commit is contained in:
parent
52c8c0b0de
commit
dd32a395f7
16 changed files with 501 additions and 156 deletions
|
|
@ -70,3 +70,76 @@ pub fn init_tracing() {
|
|||
pub fn state_dir() -> String {
|
||||
std::env::var("HYPERHIVE_STATE_DIR").unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Where this agent's forge token is read from, first match wins:
|
||||
/// `HIVE_FORGE_TOKEN_FILE` (the copy the agent fetched from the swarm secret
|
||||
/// store, set by `nix/agent-modules/forge-token.nix`), then
|
||||
/// `<state_dir>/forge-token` (the file the hive used to write, still the only
|
||||
/// copy on an agent without a store identity).
|
||||
#[must_use]
|
||||
pub fn forge_token_paths(state_dir: &str) -> Vec<std::path::PathBuf> {
|
||||
let mut paths = Vec::with_capacity(2);
|
||||
if let Ok(fetched) = std::env::var("HIVE_FORGE_TOKEN_FILE")
|
||||
&& !fetched.is_empty()
|
||||
{
|
||||
paths.push(std::path::PathBuf::from(fetched));
|
||||
}
|
||||
paths.push(std::path::Path::new(state_dir).join("forge-token"));
|
||||
paths
|
||||
}
|
||||
|
||||
/// The first non-empty token among `paths`, trimmed. `None` when none of
|
||||
/// them holds one.
|
||||
#[must_use]
|
||||
pub fn read_first_token(paths: &[std::path::PathBuf]) -> Option<String> {
|
||||
paths.iter().find_map(|p| {
|
||||
let t = std::fs::read_to_string(p).ok()?;
|
||||
let t = t.trim();
|
||||
(!t.is_empty()).then(|| t.to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod token_tests {
|
||||
use super::read_first_token;
|
||||
|
||||
fn scratch(tag: &str) -> std::path::PathBuf {
|
||||
let ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_nanos());
|
||||
let dir = std::env::temp_dir().join(format!("hive-forge-notify-token-{tag}-{ts}"));
|
||||
std::fs::create_dir_all(&dir).expect("create scratch dir");
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fetched_token_wins_over_the_state_file() {
|
||||
let dir = scratch("wins");
|
||||
let (fetched, state) = (dir.join("fetched"), dir.join("forge-token"));
|
||||
std::fs::write(&fetched, "new\n").expect("write");
|
||||
std::fs::write(&state, "old\n").expect("write");
|
||||
assert_eq!(read_first_token(&[fetched, state]).as_deref(), Some("new"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_or_empty_fetched_token_falls_back_to_the_state_file() {
|
||||
let dir = scratch("fallback");
|
||||
let (fetched, state) = (dir.join("fetched"), dir.join("forge-token"));
|
||||
std::fs::write(&state, "old\n").expect("write");
|
||||
assert_eq!(
|
||||
read_first_token(&[fetched.clone(), state.clone()]).as_deref(),
|
||||
Some("old")
|
||||
);
|
||||
std::fs::write(&fetched, "\n").expect("write");
|
||||
assert_eq!(read_first_token(&[fetched, state]).as_deref(), Some("old"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_token_anywhere_is_none() {
|
||||
let dir = scratch("none");
|
||||
assert_eq!(read_first_token(&[dir.join("a"), dir.join("b")]), None);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue