Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-secret-client/src/github.rs
atlas f8a8acae93 github swarm bao: address argus review on #4892
- docs/web-ui/README.md: drop the removed Credentials tile from the
  H0M3 hub list.
- api-error.ts, hive-warn.js: rewrite comments pointing at
  dashboard/src/credentials.js and credentials.html, now deleted, to
  state what the code does instead.
- swarm-secret-client/src/github.rs: correct the Credential.value doc
  to the actual read command (bao kv get -format=json | jq
  .data.data.value), keeping the load-bearing-field-name point.
- github-token.nix, agent-github-bao.nix, LinkGithubAccountForm.tsx:
  restate added comments as current behaviour instead of changelog
  wording ("has always had", "holds the token now").

Refs #4347
2026-10-02 17:54:45 +02:00

129 lines
4.5 KiB
Rust

//! The GitHub agreement: where an agent's GitHub personal access token lives in
//! the store, and what the object at that path holds.
//!
//! An operator hands `swarm-controller` the token, the controller stores it at
//! [`account_path`], and `nix/agent-modules/github-token.nix` reads it back
//! under the agent's own certificate. One token per agent, so one path and no
//! directory to list. Neither end is senior, so both halves are stated once,
//! here, beside [`crate::forge`].
use serde::{Deserialize, Serialize};
use crate::{
Error,
path::{Kind, principal_prefix},
};
/// The path holding `agent`'s GitHub token.
///
/// A flat leaf under the agent's prefix, like
/// [`crate::forge::agent_token_path`], so the agent's own read stanza
/// ([`crate::policy::render_agent`]) already covers it.
///
/// # Errors
/// [`Error::PathSegment`] when `agent` contains anything but `[A-Za-z0-9_-]`,
/// which is what keeps one agent's name from addressing another agent's secret.
pub fn account_path(agent: &str) -> Result<String, Error> {
let prefix = principal_prefix(Kind::Agent, agent)?;
Ok(format!("{prefix}/github-token"))
}
/// What an [`account_path`] holds.
///
/// No `Debug` derive: `value` is a live GitHub credential, and a derived
/// `Debug` is one `{:?}` away from a log line.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Credential {
/// The token. `github-token.nix` reads the store with
/// `bao kv get -format=json` and pulls `.data.data.value` out with
/// `jq`, so this name is load-bearing for a reader this crate does not
/// control.
pub value: String,
}
impl std::fmt::Debug for Credential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Credential")
.field("value", &"<redacted>")
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn covered_by(policy: &str, path: &str) -> bool {
policy.lines().any(|line| {
line.strip_prefix("path \"")
.and_then(|rest| rest.split_once("\" {"))
.and_then(|(p, _)| p.strip_suffix('*'))
.is_some_and(|prefix| format!("secret/data/{path}").starts_with(prefix))
})
}
#[test]
fn the_token_lands_under_the_agent_prefix() {
// Spelled out: `github-token.nix` spells the same string.
assert_eq!(
account_path("atlas").expect("a plain name is legal"),
"swarm/agents/atlas/github-token"
);
}
#[test]
fn a_traversal_in_the_agent_name_is_refused() {
for bad in ["../argus", "a/b", "atlas/../argus", "a.b", ""] {
let e = account_path(bad).expect_err("a traversal is not legal");
assert!(matches!(e, Error::PathSegment { kind: "agent", .. }), "{e}");
}
// The control: the legal charset stays reachable.
assert!(account_path("a-b_C9").is_ok());
}
#[test]
fn the_agents_own_read_grant_covers_the_path() {
let path = account_path("atlas").expect("legal");
let own = crate::policy::render_agent("atlas").expect("legal");
assert!(
covered_by(&own, &path),
"no stanza in\n{own}\ncovers {path}"
);
// The control: another agent's grant does not reach it.
let other = crate::policy::render_agent("argus").expect("legal");
assert!(!covered_by(&other, &path), "argus's policy reaches {path}");
}
#[test]
fn no_github_key_is_also_a_directory() {
// KV-v2 cannot hold a key whose name is also a prefix of another key.
let key = account_path("atlas").expect("legal");
for other in [
crate::forge::agent_token_path("atlas").expect("legal"),
crate::forge::accounts_dir("atlas").expect("legal"),
] {
assert_ne!(key, other);
assert!(!other.starts_with(&format!("{key}/")), "{other}");
assert!(!key.starts_with(&format!("{other}/")), "{key}");
}
}
#[test]
fn the_value_field_matches_what_the_nix_reader_asks_for() {
let json = serde_json::to_string(&Credential {
value: "t".to_owned(),
})
.expect("a String serialises");
assert_eq!(json, r#"{"value":"t"}"#);
}
#[test]
fn debug_never_prints_the_value() {
let c = Credential {
value: "0123456789abcdef".to_owned(),
};
let shown = format!("{c:?}");
assert!(!shown.contains("0123456789abcdef"), "{shown}");
assert!(shown.contains("redacted"), "{shown}");
}
}