Watch
0
0
Fork
You've already forked hyperhive
0

github: PATs live in swarm bao; the agent fetches them itself

An operator links an agent's GitHub personal access token in the swarm UI
(LinkGithubAccountForm, "link github account" on /agents). swarm-controller's
PUT /api/hives/{hive}/agents/{agent}/github-account stores it at
swarm/agents/<agent>/github-token (swarm_secret_client::github), a flat leaf
under the agent's prefix that the agent's existing read grant already covers:
no policy change, and no list grant, since there is one token per agent.

In the agent, hive-agent-github-token (oneshot + 2-minute timer, as the agent
user, under its own store certificate, ordered before hive-github-notify)
reads that path and writes <state>/github-token, 0600 and agent-owned, the
file the gh wrapper, git credential helper and hive-github-notify already
read. It replaces the file by rename only when the bytes changed and never
deletes it: a hive-written github-token stays until a token is linked in the
swarm UI. It is installed only with a store address and
services.hyperhive.agent.github.enable.

Removed: the dashboard's CR3D3NTIALS page (credentials.html/js/css, its
build entries and H0M3 tile; GITHUB was its only tab), hive-c0re's
dashboard/matrix_accounts.rs with GET/POST /api/github-account,
priv_client::write_agent_github_token, the host socket's
SetAgentGithubToken and `hivectl github set-token`, and hive-priv's
WriteAgentGithubToken with write_agent_state_file, its only caller gone.

Docs: integrations/github.md and swarm/ui.md describe the swarm path,
swarm/credentials.md gains the store-path row, and the hive UI docs,
hivectl docs and security.md's hive-priv table drop the removed pieces.

Closes #4347
This commit is contained in:
atlas 2026-10-02 17:48:27 +02:00
commit 8e23feb01b
41 changed files with 804 additions and 846 deletions

View file

@ -407,11 +407,6 @@ async fn exec(
paused,
} => exec_set_agent_paused(agent_name, paused),
PrivRequest::WriteAgentGithubToken {
ref agent_name,
ref token,
} => write_github_token(agent_name, token),
PrivRequest::RegisterCiRunner { ref token } => register_ci_runner(token).await,
PrivRequest::ControlInfraContainer { container, action } => {
@ -547,12 +542,6 @@ fn exec_set_agent_paused(agent_name: &str, paused: bool) -> Result<(String, Stri
set_agent_paused(agent_name, paused)
}
/// `WriteAgentGithubToken`.
fn write_github_token(agent_name: &str, token: &str) -> Result<(String, String)> {
validate_agent_name(agent_name)?;
write_agent_state_file(agent_name, "github-token", &format!("{token}\n"))
}
/// `EnsureAgentSubvolume`.
async fn exec_ensure_agent_subvolume(agent_name: &str) -> Result<(String, String)> {
validate_agent_name(agent_name)?;
@ -1486,22 +1475,6 @@ fn publish_file(path: &Path, content: &[u8], mode: u32, owner: Option<(u32, u32)
staged.publish()
}
/// Shared helper for the `WriteAgent*Token` requests.
/// Writes `content` to `AGENT_STATE_ROOT/<agent_name>/state/<filename>`,
/// chowns to the agent user (derived from the state dir's existing owner),
/// and chmods 0600. Running as root (hive-priv), so this succeeds
/// regardless of the file's prior owner/permissions.
fn write_agent_state_file(
agent_name: &str,
filename: &str,
content: &str,
) -> Result<(String, String)> {
let state_dir = PathBuf::from(AGENT_STATE_ROOT)
.join(agent_name)
.join("state");
write_agent_dir_file(agent_name, &state_dir, filename, content)
}
/// Create or remove an agent's pause marker under its harness dir. The
/// marker is written empty and chowned to the harness dir's owner (the
/// agent), matching how the harness itself would have created it.
@ -1536,10 +1509,9 @@ fn remove_marker_in(dir: &Path, filename: &str) -> Result<()> {
}
/// Write `content` to `dir/filename` as root, chowning the result to `dir`'s
/// owner so the agent process can read it back. Shared by the credential
/// writes (which target `state/`) and the pause marker (which targets
/// `harness/`) — both write into a directory owned by the agent, which is
/// precisely why they need hive-priv at all.
/// owner so the agent process can read it back. Its caller is the pause
/// marker, in `harness/`, a directory owned by the agent, which is precisely
/// why it needs hive-priv at all.
///
/// The file is published through a [`StagedFile`], so a reader woken by its
/// appearance cannot catch it empty or half-written: several of these paths
@ -1563,8 +1535,7 @@ fn write_agent_dir_file(
// yet (container being provisioned for the first time), the newly created dir
// is root:root. The `stat state_dir` chown below will then see uid=0 and
// leave the file root-owned (0600). The agent won't be able to read it until
// its lifecycle completes. If that happens, a `systemctl restart hive-c0re`
// after provisioning will re-mint and re-write the token correctly.
// its lifecycle completes.
std::fs::create_dir_all(&state_dir)
.with_context(|| format!("create state dir {}", state_dir.display()))?;
@ -1592,7 +1563,7 @@ fn write_agent_dir_file(
agent = %agent_name,
path = %path.display(),
error = %e,
"write_agent_state_file: fchown failed"
"write_agent_dir_file: fchown failed"
);
}
}
@ -1600,7 +1571,7 @@ fn write_agent_dir_file(
tracing::warn!(
agent = %agent_name,
error = %e,
"write_agent_state_file: stat state_dir failed, leaving file root-owned"
"write_agent_dir_file: stat state_dir failed, leaving file root-owned"
);
}
}