agents keep their github token in /run, fetched on every start, not in the state dir
This commit is contained in:
parent
adc61c9043
commit
4bd02ea14d
3 changed files with 310 additions and 78 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue