Watch
0
0
Fork
You've already forked hyperhive
0

agents keep their github token in /run, fetched on every start, not in the state dir

This commit is contained in:
atlas 2026-11-15 02:04:04 +01:00 • committed by mara
commit 4bd02ea14d
3 changed files with 310 additions and 78 deletions

View file

@ -3,14 +3,14 @@
//! list, per-thread summary, todo upsert on the harness's in-agent socket,
//! mark read on the source.
//!
//! Takes no arguments. `HYPERHIVE_STATE_DIR` holds the PAT
//! (`github-token`, fetched from the swarm secret store by
//! Takes no arguments. `HIVE_GITHUB_TOKEN_FILE` names the file holding the PAT
//! (fetched from the swarm secret store into `/run` by
//! `nix/agent-modules/github-token.nix` — see `docs/2-handbook/external-accounts/github.md`)
//! and `HIVE_AGENT_SOCKET` is the harness's todo socket.
//! Having a PAT *is* the opt-in: with no token the poller logs why and
//! exits 0, so deploying this unit to an agent that never gets one costs a
//! settled process rather than a restart loop. Once polling, it re-reads
//! `github-token` on every tick, so a rotated PAT takes effect on the next
//! the token file on every tick, so a rotated PAT takes effect on the next
//! poll without a restart.
//!
//! ⚠️ Reading the notification stream needs the **`notifications` scope**
@ -37,7 +37,18 @@ async fn main() {
info!(socket = %socket.display(), "hive-github-notify starting");
// Returns only when no PAT ever arrives; otherwise loops forever.
github_loop(hive_forge_notify::state_dir(), socket).await;
github_loop(
token_path(std::env::var_os("HIVE_GITHUB_TOKEN_FILE")),
socket,
)
.await;
}
/// The token file the poller reads: `HIVE_GITHUB_TOKEN_FILE`, and nothing
/// else. An unset or empty value means no file is configured, so the poller
/// settles instead of looking for a token anywhere else.
fn token_path(var: Option<std::ffi::OsString>) -> Option<std::path::PathBuf> {
var.filter(|v| !v.is_empty()).map(std::path::PathBuf::from)
}
/// Waits for the PAT to appear: the agent's store fetch writes the token
@ -46,15 +57,21 @@ async fn main() {
/// than after a restart. Gives up — returning, so the process exits 0
/// rather than restart-looping — when no PAT ever arrives, which is the
/// common case for an agent that has the unit but no account.
async fn github_loop(state_dir: String, socket: std::path::PathBuf) {
let token_path = format!("{state_dir}/github-token");
async fn github_loop(token_path: Option<std::path::PathBuf>, socket: std::path::PathBuf) {
let Some(token_path) = token_path else {
info!("forge_notify: HIVE_GITHUB_TOKEN_FILE is not set — github disabled");
return;
};
let Some(token) = wait_for_token("github token", || async {
let text = tokio::fs::read_to_string(&token_path).await.ok()?;
Some(text.trim().to_owned()).filter(|t| !t.is_empty())
})
.await
else {
debug!("forge_notify: no github token at {token_path} — github disabled");
debug!(
"forge_notify: no github token at {} — github disabled",
token_path.display()
);
return;
};
@ -122,7 +139,7 @@ async fn poll_tick(
}
/// The PAT in use and the source built from it. The agent's store fetch
/// replaces `github-token` by atomic rename when the PAT rotates and never
/// replaces the token file by atomic rename when the PAT rotates and never
/// deletes it, so a missing or empty file means "not readable right now"
/// and keeps the current token.
struct Pat {
@ -164,7 +181,7 @@ mod tests {
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::sync::mpsc;
use super::{Pat, poll_tick};
use super::{Pat, poll_tick, token_path};
fn scratch(tag: &str) -> std::path::PathBuf {
let ts = std::time::SystemTime::now()
@ -236,6 +253,17 @@ mod tests {
path.starts_with("/notifications")
}
#[test]
fn the_token_file_is_the_env_value_and_nothing_else() {
use std::ffi::OsString;
assert_eq!(
token_path(Some(OsString::from("/run/x/token"))),
Some(std::path::PathBuf::from("/run/x/token"))
);
assert_eq!(token_path(Some(OsString::new())), None);
assert_eq!(token_path(None), None);
}
#[tokio::test]
async fn a_rotated_token_is_sent_on_the_next_poll() {
let (base, mut rx) = stub().await;

View file

@ -119,7 +119,7 @@ impl Source for GithubSource {
{
warn!(
"forge_notify: github refused the PAT (401) — no notifications \
until github-token holds a working one"
until the token file holds a working one"
);
} else {
debug!("forge_notify: github poll status {status}");

View file

@ -63,7 +63,7 @@ pub fn init_tracing() {
}
/// Read `HYPERHIVE_STATE_DIR`, the directory holding the agent's
/// credentials (`forge-token`, `github-token`). Empty when unset, which
/// credentials (`forge-token`). Empty when unset, which
/// makes the token paths relative and the read fail — the callers treat
/// that as "not configured" and settle.
#[must_use]