diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index eef8d729..3d0d69ef 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -70,6 +70,15 @@ enum Cmd { #[command(subcommand)] cmd: MatrixCmd, }, + /// GitHub account provisioning: write an operator-supplied personal + /// access token (PAT) into an agent's `hyperhive.githubAccount` token + /// file so its `gh` wrapper + git credential helper can authenticate. + /// Unlike forge/matrix there is no account creation — the operator + /// supplies a PAT for an existing GitHub account. + Github { + #[command(subcommand)] + cmd: GithubCmd, + }, /// Gateway htpasswd user management. Add, remove, or list users in /// an htpasswd file used by the gateway's HTTP Basic auth /// (`services.hyperhive.gateway.auth`). Credentials are stored as @@ -421,6 +430,28 @@ enum MatrixCmd { }, } +#[derive(Subcommand)] +enum GithubCmd { + /// Write a GitHub PAT into ``'s state dir (`github-token`, 0600, + /// agent-owned) via hive-priv. The agent must declare + /// `hyperhive.githubAccount` (its `tokenFile` pointing at this path) for + /// the `gh` wrapper + git credential helper to pick it up. The token is + /// read live at invocation, so no rebuild/restart is needed. Prefer + /// `--token-stdin`: an inline `--token` is visible in shell history + + /// process listings. + SetToken { + /// Logical agent name (the container/agent name). + agent: String, + /// The PAT value inline. Mutually exclusive with `--token-stdin`. + #[arg(long)] + token: Option, + /// Read the PAT from stdin (trailing newline stripped). Mutually + /// exclusive with `--token`. + #[arg(long, conflicts_with = "token")] + token_stdin: bool, + }, +} + // Default htpasswd file path — the host-side location of the gateway's // credential store, pre-created by a tmpfiles rule when // `services.hyperhive.gateway.auth.enable = true`. Literal lives in @@ -618,6 +649,13 @@ async fn main() -> Result<()> { MatrixCmd::ResetPassword { name } => matrix_reset_password(&name).await, MatrixCmd::Invite { user, room } => matrix_invite(&user, room.as_deref()).await, }, + Cmd::Github { cmd } => match cmd { + GithubCmd::SetToken { + agent, + token, + token_stdin, + } => github_set_token(&agent, token, token_stdin).await, + }, Cmd::Gateway { cmd } => match cmd { GatewayCmd::CreateUser { file, @@ -1124,6 +1162,34 @@ fn choom(name: &str, resume_session: Option<&str>) -> Result<()> { Err(anyhow::anyhow!("exec machinectl: {err}")) } +/// `hivectl github set-token `: write an operator-supplied GitHub PAT +/// into the agent's `github-token` state file (0600, agent-owned) via +/// hive-priv, so the agent's `gh` wrapper + git credential helper can +/// authenticate. Read live at invocation, so no rebuild/restart is needed. +async fn github_set_token(agent: &str, token: Option, token_stdin: bool) -> Result<()> { + let token = match (token, token_stdin) { + (Some(t), _) => t, + (None, true) => { + let mut s = String::new(); + std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; + s.trim_end_matches(['\n', '\r']).to_owned() + } + (None, false) => bail!( + "provide the PAT via --token or --token-stdin (stdin preferred — \ + an inline token is visible in shell history + process listings)" + ), + }; + if token.is_empty() { + bail!("refusing to write an empty GitHub token for agent '{agent}'"); + } + hive_c0re::priv_client::write_agent_github_token(agent, &token).await?; + println!( + "wrote github-token for agent '{agent}' \ + (read live by the gh wrapper / git credential helper — no rebuild needed)" + ); + Ok(()) +} + async fn forge_create_user(name: &str, password: Option<&str>, password_stdin: bool) -> Result<()> { if !hive_c0re::forge::is_present().await { bail!( diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index c8337f21..4f2d8b31 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -281,6 +281,19 @@ pub async fn write_agent_matrix_token( .await?) } +/// Write a GitHub personal access token (PAT) for `agent_name` via hive-priv +/// (running as root). Writes `/github-token` 0600, chowned to the agent +/// user so the `gh` wrapper / git credential helper can read it from inside the +/// container. Single account per agent — no account suffix. The token value is +/// operator-supplied (for the agent's `hyperhive.githubAccount`). +pub async fn write_agent_github_token(agent_name: &str, token: &str) -> Result<()> { + ok(call(&PrivRequest::WriteAgentGithubToken { + agent_name: agent_name.to_owned(), + token: token.to_owned(), + }) + .await?) +} + /// Restart `hive-matrix-daemon.service` inside an agent container via /// `systemctl --machine=h- restart hive-matrix-daemon.service`. /// Non-fatal: callers should handle errors gracefully — if the container is diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 93293183..2977f8e9 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -289,6 +289,14 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String, Ok(res) } + PrivRequest::WriteAgentGithubToken { + ref agent_name, + ref token, + } => { + validate_agent_name(agent_name)?; + write_agent_state_file(agent_name, "github-token", &format!("{token}\n")) + } + PrivRequest::RestartMatrixDaemon { ref agent_name } => { restart_matrix_daemon(agent_name).await } diff --git a/hive-sh4re/src/priv_proto.rs b/hive-sh4re/src/priv_proto.rs index 89fef768..71dd4929 100644 --- a/hive-sh4re/src/priv_proto.rs +++ b/hive-sh4re/src/priv_proto.rs @@ -411,6 +411,21 @@ pub enum PrivRequest { homeserver: Option, }, + /// Write `github-token` into `AGENT_STATE_ROOT//state/github-token`. + /// + /// The operator-supplied GitHub personal access token (PAT) for the + /// agent's `hyperhive.githubAccount`. Same write semantics as + /// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir + /// if absent, writes the file 0600, and chowns it to the agent so the + /// `gh` wrapper / git credential helper can read it. No account suffix + /// (single GitHub account per agent). + WriteAgentGithubToken { + /// Logical agent name (validated by `validate_agent_name`). + agent_name: String, + /// PAT value. hive-priv appends a trailing newline before writing. + token: String, + }, + /// Restart `hive-matrix-daemon.service` inside an agent container via /// `systemctl --machine=h- restart hive-matrix-daemon.service`. /// Used by hive-c0re to kick the daemon after a successful token write