refactor(#2352): route hivectl github set-token through a daemon wire command

This commit is contained in:
damocles 2026-07-14 22:17:09 +02:00
commit 6c654921a0
3 changed files with 49 additions and 14 deletions

View file

@ -696,7 +696,7 @@ async fn main() -> Result<()> {
agent, agent,
token, token,
token_stdin, token_stdin,
} => github_set_token(&agent, token, token_stdin).await, } => github_set_token(&socket, &agent, token, token_stdin).await,
}, },
Cmd::Gateway { cmd } => match cmd { Cmd::Gateway { cmd } => match cmd {
GatewayCmd::CreateUser { GatewayCmd::CreateUser {
@ -1234,7 +1234,15 @@ fn choom(name: &str, resume_session: Option<&str>) -> Result<()> {
/// into the agent's `github-token` state file (0600, agent-owned) via /// into the agent's `github-token` state file (0600, agent-owned) via
/// hive-priv, so the agent's `gh` wrapper + git credential helper can /// hive-priv, so the agent's `gh` wrapper + git credential helper can
/// authenticate. Read live at invocation, so no rebuild/restart is needed. /// authenticate. Read live at invocation, so no rebuild/restart is needed.
async fn github_set_token(agent: &str, token: Option<String>, token_stdin: bool) -> Result<()> { async fn github_set_token(
socket: &Path,
agent: &str,
token: Option<String>,
token_stdin: bool,
) -> Result<()> {
// Resolve + validate the token client-side (inline flag or stdin read);
// the daemon never touches this process's stdin. Persistence happens
// daemon-side via the privileged helper.
let token = match (token, token_stdin) { let token = match (token, token_stdin) {
(Some(t), _) => t, (Some(t), _) => t,
(None, true) => { (None, true) => {
@ -1250,12 +1258,15 @@ async fn github_set_token(agent: &str, token: Option<String>, token_stdin: bool)
if token.is_empty() { if token.is_empty() {
bail!("refusing to write an empty GitHub token for agent '{agent}'"); bail!("refusing to write an empty GitHub token for agent '{agent}'");
} }
hive_c0re::priv_client::write_agent_github_token(agent, &token).await?; daemon_request(
println!( socket,
"wrote github-token for agent '{agent}' \ hive_host_sock::HostRequest::SetAgentGithubToken {
(read live by the gh wrapper / git credential helper no rebuild needed)" agent: agent.to_owned(),
); token,
Ok(()) },
"github",
)
.await
} }
async fn forge_create_user( async fn forge_create_user(
@ -1269,26 +1280,31 @@ async fn forge_create_user(
// agent-vs-operator branch, and token persistence now live in the // agent-vs-operator branch, and token persistence now live in the
// daemon handler. // daemon handler.
let password = resolve_password(password, password_stdin)?; let password = resolve_password(password, password_stdin)?;
forge_request( daemon_request(
socket, socket,
hive_host_sock::HostRequest::ForgeCreateUser { hive_host_sock::HostRequest::ForgeCreateUser {
name: name.to_owned(), name: name.to_owned(),
password, password,
}, },
"forge",
) )
.await .await
} }
/// Send a forge provisioning request to the daemon and print its result /// Send a provisioning request to the daemon and print its result lines.
/// lines. Mirrors [`matrix_request`] — the daemon owns the provisioning /// The daemon owns the provisioning logic; hivectl just relays the outcome,
/// logic; hivectl just relays the outcome. /// prefixing any error with `label` (e.g. `forge` / `github`).
async fn forge_request(socket: &Path, req: hive_host_sock::HostRequest) -> Result<()> { async fn daemon_request(
socket: &Path,
req: hive_host_sock::HostRequest,
label: &str,
) -> Result<()> {
let resp = hive_c0re::client::request(socket, req) let resp = hive_c0re::client::request(socket, req)
.await .await
.with_context(|| format!("connect to daemon socket {}", socket.display()))?; .with_context(|| format!("connect to daemon socket {}", socket.display()))?;
if !resp.ok { if !resp.ok {
bail!( bail!(
"forge: {}", "{label}: {}",
resp.error.as_deref().unwrap_or("unknown error") resp.error.as_deref().unwrap_or("unknown error")
); );
} }

View file

@ -196,6 +196,9 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
HostRequest::ForgeCreateUser { name, password } => { HostRequest::ForgeCreateUser { name, password } => {
handle_forge_create_user(name, password.as_deref()).await? handle_forge_create_user(name, password.as_deref()).await?
} }
HostRequest::SetAgentGithubToken { agent, token } => {
handle_set_agent_github_token(agent, token).await?
}
}) })
} }
.await; .await;
@ -395,6 +398,16 @@ async fn handle_forge_create_user(name: &str, password: Option<&str>) -> Result<
Ok(HostResponse::messages(out)) Ok(HostResponse::messages(out))
} }
async fn handle_set_agent_github_token(agent: &str, token: &str) -> Result<HostResponse> {
crate::priv_client::write_agent_github_token(agent, token)
.await
.with_context(|| format!("write github-token for agent {agent}"))?;
Ok(HostResponse::messages(vec![format!(
"wrote github-token for agent '{agent}' \
(read live by the gh wrapper / git credential helper no rebuild needed)"
)]))
}
async fn handle_matrix_sync_admin() -> Result<HostResponse> { async fn handle_matrix_sync_admin() -> Result<HostResponse> {
require_matrix_present().await?; require_matrix_present().await?;
let register_token = let register_token =

View file

@ -158,6 +158,12 @@ pub enum HostRequest {
#[serde(default)] #[serde(default)]
password: Option<String>, password: Option<String>,
}, },
/// Write (or overwrite) an agent's GitHub PAT under its state dir, via
/// the privileged helper. Daemon-side equivalent of `hivectl github
/// set-token`. `token` is resolved + non-empty-validated client-side
/// (inline flag or stdin); the daemon just persists it. Read live by
/// the agent's `gh` wrapper / git credential helper — no rebuild needed.
SetAgentGithubToken { agent: String, token: String },
} }
/// Selects which container classes a hive-wide [`HostRequest::Stop`] / /// Selects which container classes a hive-wide [`HostRequest::Stop`] /