From 303037689e7b9067406495d9490902dcc4fb694c Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 10:30:06 +0200 Subject: [PATCH 01/10] wip(#1970): add hyperhive.githubAccount nix option (single-account, nullable) --- nix/templates/harness-base.nix | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 39199562..bff583a5 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -624,6 +624,76 @@ in ''; }; + options.hyperhive.githubAccount = lib.mkOption { + type = lib.types.nullOr ( + lib.types.submodule { + options = { + username = lib.mkOption { + type = lib.types.str; + example = "the-sword-above"; + description = '' + GitHub login the agent acts as. Used as the username for + `git push` over HTTPS and shown to the agent via the + `HIVE_GITHUB_USER` environment variable. Should be a + dedicated bot account, never a human's. + ''; + }; + tokenFile = lib.mkOption { + type = lib.types.str; + example = "/agents/damocles/state/github-token"; + description = '' + Path to the file holding this account's personal access + token (PAT). The token *value* is never in nix --- the + provisioner (dashboard credentials tab, or `hivectl`) + writes an operator-supplied PAT here (0600, agent-owned), + the same contract as `matrixAccounts..tokenFile`. + The `gh` wrapper and the git credential helper read the + token from this path at invocation time, so a PAT pasted + mid-session takes effect with no rebuild. `gh` / `git push` + simply fail unauthenticated until the file exists. + + Keep the PAT minimally scoped (only the repos/scopes the + agent's workflow needs): the agent has passwordless sudo, + so a compromised agent can act as the account within the + token's scopes --- scope is the real blast-radius limiter. + ''; + }; + host = lib.mkOption { + type = lib.types.str; + default = "github.com"; + example = "github.example.com"; + description = '' + GitHub host. Defaults to `github.com`; set it for a GitHub + Enterprise instance. Drives both the `gh` API host + (`GH_HOST`) and the git credential helper's URL match. + ''; + }; + }; + } + ); + default = null; + example = lib.literalExpression '' + { + username = "the-sword-above"; + tokenFile = "/agents/damocles/state/github-token"; + } + ''; + description = '' + Give the agent a managed GitHub account: a `gh` CLI wrapper and a + git credential helper, both authenticated from an operator-supplied + PAT, so the agent can run `gh` API calls and `git push` to GitHub as + the configured login without any manual `gh auth login` dance. + + `null` (the default) leaves GitHub off entirely --- no `gh` wrapper, + no credential helper, no env. When set, the token is supplied out of + band (dashboard credentials tab / `hivectl`) into `tokenFile`; nix + only carries the login + host, never the secret. + + Single account per agent by design (unlike `matrixAccounts`, which is + multi-account): the GitHub workflow is "this agent is this one bot". + ''; + }; + options.hyperhive.frontend.dist = lib.mkOption { type = lib.types.package; default = pkgs.hyperhive-frontend; From 5fb4b9f4b02cac8f97fff82bcd19176a5e7618af Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 10:37:18 +0200 Subject: [PATCH 02/10] wip(#1970): gh wrapper + git credential helper + gated env/gitconfig for githubAccount --- nix/templates/harness-base.nix | 95 ++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 21 deletions(-) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index bff583a5..4825cb5e 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -17,6 +17,27 @@ let # from `userName` to keep them coupled. userName = config.hyperhive.user.name; homeDir = "/home/${userName}"; + # GitHub account (hyperhive.githubAccount): a `gh` wrapper + a git + # credential helper, both sourcing the PAT from HIVE_GITHUB_TOKEN_FILE at + # invocation time so a dashboard-pasted token takes effect with no rebuild. + # The scripts are static (they read env at runtime) — the account option + # only gates whether they're installed + the env is set, so the token + # value never enters the nix store. + ghWrapper = pkgs.writeShellScriptBin "gh" '' + if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then + GH_TOKEN="$(cat "''${HIVE_GITHUB_TOKEN_FILE}")" + export GH_TOKEN + fi + exec ${pkgs.gh}/bin/gh "$@" + ''; + gitCredHelper = pkgs.writeShellScriptBin "git-credential-hive-github" '' + # git credential-helper protocol: only the `get` action needs an answer. + [ "''${1:-}" = "get" ] || exit 0 + if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then + printf 'username=%s\n' "''${HIVE_GITHUB_USER:-x-access-token}" + printf 'password=%s\n' "$(cat "''${HIVE_GITHUB_TOKEN_FILE}")" + fi + ''; # Hive-wide OpenTelemetry config (host-driven; baked in per-agent by # meta.rs `otel_config`). otelCfg = config.hyperhive.otel; @@ -1558,6 +1579,15 @@ in # regardless of which profile files are sourced. NIX_REMOTE = "daemon"; } + // lib.optionalAttrs (config.hyperhive.githubAccount != null) { + # GitHub account: metadata + token-file PATH only, never the secret. + # The `gh` wrapper + git credential helper read the PAT from the file + # at invocation time (see hyperhive.githubAccount). + HIVE_GITHUB_USER = config.hyperhive.githubAccount.username; + HIVE_GITHUB_HOST = config.hyperhive.githubAccount.host; + HIVE_GITHUB_TOKEN_FILE = config.hyperhive.githubAccount.tokenFile; + GH_HOST = config.hyperhive.githubAccount.host; + } // lib.optionalAttrs (!config.hyperhive.autoCompact) { # Zero watermark disables proactive compaction; the reactive path # (compact-on-overflow) still fires when the session is truly full. @@ -1652,27 +1682,50 @@ in # we have to allow it inside the container's config as well. nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (pkgs.lib.getName pkg) [ "claude-code" ]; - environment.systemPackages = with pkgs; [ - hyperhive - claude-code - bashInteractive - coreutils-full - # procps for pkill — used by the web UI's /api/cancel to SIGINT the - # in-flight claude turn. - procps - # tea: gitea/forgejo CLI client. Configured at boot by the - # tea-login oneshot below if /state/forge-token is present, so - # claude can `tea repos create`, `tea pulls create`, etc. - tea - # jq: JSON processing in shell — useful for parsing API responses, - # forge REST calls, sqlite output, etc. - jq - # curl: HTTP client for forge REST API and other web requests. - curl - # hive-forge : CLI wrapping common Forgejo REST API operations - # (view, pr, issue, comment, assign, close, labels, branches, etc.) - (pkgs.callPackage ../packages/hive-forge-tools.nix { }) - ]; + environment.systemPackages = + with pkgs; + [ + hyperhive + claude-code + bashInteractive + coreutils-full + # procps for pkill — used by the web UI's /api/cancel to SIGINT the + # in-flight claude turn. + procps + # tea: gitea/forgejo CLI client. Configured at boot by the + # tea-login oneshot below if /state/forge-token is present, so + # claude can `tea repos create`, `tea pulls create`, etc. + tea + # jq: JSON processing in shell — useful for parsing API responses, + # forge REST calls, sqlite output, etc. + jq + # curl: HTTP client for forge REST API and other web requests. + curl + # hive-forge : CLI wrapping common Forgejo REST API operations + # (view, pr, issue, comment, assign, close, labels, branches, etc.) + (pkgs.callPackage ../packages/hive-forge-tools.nix { }) + ] + ++ lib.optionals (config.hyperhive.githubAccount != null) [ + # gh wrapper + git credential helper for hyperhive.githubAccount. + # (No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the real + # binary path, so it can't be shadowed.) + ghWrapper + gitCredHelper + ]; + + # Wire the GitHub credential helper for `git push` over HTTPS to the + # configured host. Host-scoped (github.com or a GHE host), so it never + # touches the forge (localhost:3000) or any other remote. Gated on the + # account; the helper reads the PAT from HIVE_GITHUB_TOKEN_FILE at + # invocation (see hyperhive.githubAccount). System /etc/gitconfig merges + # under the agent's ~/.gitconfig (safe.directory), so this is additive. + environment.etc = lib.optionalAttrs (config.hyperhive.githubAccount != null) { + "gitconfig".text = '' + [credential "https://${config.hyperhive.githubAccount.host}"] + helper = hive-github + username = ${config.hyperhive.githubAccount.username} + ''; + }; # One-shot: tea config.yml from the seeded forge token. Shape # contract (always exit 0, no set -e, skip-silently, re-runnable): From 80ef7d81519b661d42f63026fc00d0b1164b6178 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 10:45:09 +0200 Subject: [PATCH 03/10] wip(#1970): github-token injection path (priv wire + hive-priv handler + priv_client + hivectl github set-token) --- hive-c0re/src/bin/hivectl.rs | 66 ++++++++++++++++++++++++++++++++++++ hive-c0re/src/priv_client.rs | 13 +++++++ hive-priv/src/main.rs | 8 +++++ hive-sh4re/src/priv_proto.rs | 15 ++++++++ 4 files changed, 102 insertions(+) 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 From d804859128ce531c19a1934d4888761d8e41a955 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 10:49:01 +0200 Subject: [PATCH 04/10] docs(#1970): document githubAccount + gh/git integration + hivectl github set-token --- CLAUDE.md | 2 ++ README.md | 14 +++++++++ docs/github.md | 72 +++++++++++++++++++++++++++++++++++++++++++ docs/tools/hivectl.md | 19 ++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 docs/github.md diff --git a/CLAUDE.md b/CLAUDE.md index 37322c3a..aab72fdc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -113,6 +113,8 @@ read them à la carte. - **"How does the matrix-tuwunel container work? What about fluffychat-web and per-agent matrix accounts?"** → [`docs/matrix.md`](docs/matrix.md). +- **"How do I give an agent a GitHub account (`gh` + `git push`)? + How is the PAT injected?"** → [`docs/github.md`](docs/github.md). - **"How does DNS resolution work in agent containers? What's the bridge network for?"** → [`docs/network.md`](docs/network.md). - **"How do I connect two hives into a swarm? How do I declare peer diff --git a/README.md b/README.md index b83d55a4..ca99fdac 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,20 @@ The hive-internal account is always named `main` (synthesized from `hyperhive.ma For more details see [`docs/matrix.md`](docs/matrix.md). +### GitHub account + +`hyperhive.githubAccount` gives an agent a managed GitHub identity — a `gh` CLI wrapper and `git push` over HTTPS, both authenticated as a configured bot account: + +```nix +hyperhive.githubAccount = { + username = "the-sword-above"; + tokenFile = "/agents/myagent/state/github-token"; # PAT provisioned out-of-band + # host = "github.com"; # default; set for GHE +}; +``` + +The PAT value is never in nix; write it with `hivectl github set-token --token-stdin`. Both the `gh` wrapper and the git credential helper read the token live, so a rotated PAT takes effect with no rebuild. Single account per agent. For more details see [`docs/github.md`](docs/github.md). + ## Operator CLI `hivectl` is the operator-facing host CLI for ad-hoc administration that diff --git a/docs/github.md b/docs/github.md new file mode 100644 index 00000000..3212acf1 --- /dev/null +++ b/docs/github.md @@ -0,0 +1,72 @@ +# GitHub accounts + +Give an agent a managed GitHub identity — a `gh` CLI and `git push` over +HTTPS, both authenticated as a configured bot account — so it can run +GitHub API calls and push commits without any manual `gh auth login`. + +This mirrors the [matrix account](matrix.md) pattern: nix carries the +login + host, never the secret; an operator-supplied personal access +token (PAT) is injected out of band into the agent's state dir. + +## Config option + +Declare `hyperhive.githubAccount` in the agent's `agent.nix`: + +```nix +hyperhive.githubAccount = { + username = "the-sword-above"; # the bot login + tokenFile = "/agents//state/github-token"; # where the PAT lives + # host = "github.com"; # default; set for GHE +}; +``` + +`null` (the default) leaves GitHub off entirely — no `gh` wrapper, no +credential helper, no env. Single account per agent by design (unlike +`matrixAccounts`, which is multi-account): the workflow is "this agent is +this one bot". + +The token **value** is never in nix. `tokenFile` only names the path; the +PAT is written there separately (see [Provisioning](#provisioning)). + +## How the agent uses it + +When `githubAccount` is set, the container gets: + +- **A `gh` wrapper** on `PATH` (shadowing the raw `gh`) that exports + `GH_TOKEN` from the token file at invocation, then execs real `gh`. So + `gh pr create`, `gh api …`, etc. just work as the bot. +- **A git credential helper** (`git-credential-hive-github`), wired via a + host-scoped `/etc/gitconfig` entry for `https://`, so + `git push https://github.com//` authenticates as the bot. + Host-scoped, so it never touches the forge (`localhost:3000`) or any + other remote. +- **Env**: `HIVE_GITHUB_USER`, `HIVE_GITHUB_HOST`, + `HIVE_GITHUB_TOKEN_FILE`, and `GH_HOST`. + +Both the wrapper and the credential helper read the token from the file +**at invocation time**, so a PAT written (or rotated) mid-session takes +effect immediately — no container rebuild or restart. Until the file +exists, `gh` / `git push` simply fail unauthenticated. + +## Provisioning + +The PAT is operator-supplied. Write it into the agent's token file with: + +```sh +hivectl github set-token --token-stdin # paste the PAT on stdin (preferred) +hivectl github set-token --token # inline (visible in shell history) +``` + +hive-c0re delegates the write to hive-priv, which stores the file `0600` +owned by the agent (so the container can read it) — the same credential +injection path as forge/matrix tokens. See +[hivectl → GitHub](tools/hivectl.md#github). + +## Security + +- Use a **dedicated bot account**, never a human's. +- Mint a **minimally-scoped PAT** — only the repos/scopes the agent's + workflow needs. Agents have passwordless sudo, so a compromised or + hallucinating agent can act as the account within the token's scopes; + scope is the real blast-radius limiter, and the container boundary is + the enforcement. See [security.md](security.md). diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 6af3b932..38d1c84f 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -75,6 +75,25 @@ hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a spec invite power (it owns the hive Space, so that case always works). Idempotent — already-member / already-invited is a no-op. +## GitHub + +Write an operator-supplied GitHub personal access token (PAT) into an +agent's token file so its `gh` wrapper + git credential helper can act as +the configured bot account. Unlike forge/matrix there is no account +creation — the PAT is for an existing GitHub account. The agent must +declare [`hyperhive.githubAccount`](../github.md). + +```bash +hivectl github set-token damocles --token-stdin # paste the PAT on stdin (preferred) +hivectl github set-token damocles --token # inline (visible in shell history) +``` + +- `set-token`: writes `/github-token` (`0600`, agent-owned) via + hive-priv — the same credential-injection path as forge/matrix tokens. + The `gh` wrapper / git credential helper read it live, so a freshly-set + or rotated PAT takes effect with no rebuild or restart. Refuses an empty + token. See [github.md](../github.md) for the full flow + security notes. + ## Gateway Manage users in the gateway's HTTP Basic auth htpasswd file From 1b6b307aa71c6fb5066e9932c0b0e40e8278d0f1 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 10:53:12 +0200 Subject: [PATCH 05/10] feat(#1970): add POST /api/github-account dashboard endpoint for the credentials UI --- hive-c0re/src/dashboard/matrix_accounts.rs | 39 ++++++++++++++++++++++ hive-c0re/src/dashboard/mod.rs | 4 +++ 2 files changed, 43 insertions(+) diff --git a/hive-c0re/src/dashboard/matrix_accounts.rs b/hive-c0re/src/dashboard/matrix_accounts.rs index 6d0e5cfa..e526d7fd 100644 --- a/hive-c0re/src/dashboard/matrix_accounts.rs +++ b/hive-c0re/src/dashboard/matrix_accounts.rs @@ -262,6 +262,45 @@ pub(super) async fn post_matrix_account_login(Form(f): Form) -> axum::Json(MatrixLoginResult { ok: true, user_id }).into_response() } +/// Form body for `POST /api/github-account` (urlencoded, the dashboard's +/// mutation convention). Writes the operator-supplied PAT to the agent's +/// `github-token` file. The GitHub counterpart of the matrix login form, but +/// far simpler: no account creation, no homeserver, no login modes — the +/// operator pastes a PAT for an existing account. +#[derive(Deserialize)] +pub(super) struct GithubAccountForm { + agent: String, + token: String, +} + +#[derive(Serialize)] +struct GithubAccountResult { + ok: bool, +} + +/// Provision (or refresh) an agent's GitHub PAT from the dashboard +/// credentials tab. Validates the agent name, then writes the PAT to +/// `/github-token` (`0600`, agent-owned) via hive-priv. No account +/// creation and no daemon to kick — the agent's `gh` wrapper / git credential +/// helper read the file live, so the new token takes effect immediately. +/// Operator-authenticated (dashboard). Never echoes the token back — only +/// `{ ok: true }`. +pub(super) async fn post_github_account(Form(f): Form) -> Response { + let agent = f.agent.trim(); + let token = f.token.trim(); + if !is_plain_ident(agent) { + return error_response(&format!("github-account: invalid agent {agent:?}")); + } + if token.is_empty() { + return error_response("github-account: token is required"); + } + if let Err(e) = crate::priv_client::write_agent_github_token(agent, token).await { + return error_response(&format!("github-account: write token failed: {e:#}")); + } + tracing::info!(%agent, "github-account: provisioned github PAT"); + axum::Json(GithubAccountResult { ok: true }).into_response() +} + /// POST `m.login.password` to `/_matrix/client/v3/login`. /// Returns `(access_token, user_id)`. async fn matrix_password_login( diff --git a/hive-c0re/src/dashboard/mod.rs b/hive-c0re/src/dashboard/mod.rs index 18abd4e5..b3909047 100644 --- a/hive-c0re/src/dashboard/mod.rs +++ b/hive-c0re/src/dashboard/mod.rs @@ -188,6 +188,10 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { "/api/matrix-account-login", post(matrix_accounts::post_matrix_account_login), ) + .route( + "/api/github-account", + post(matrix_accounts::post_github_account), + ) .route( "/api/cancel-reminder/{id}", post(reminders::post_cancel_reminder), From 3e5aff39eb04e365bc53823014b1d63edfa4a0ad Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 10:58:35 +0200 Subject: [PATCH 06/10] =?UTF-8?q?fix(#1970):=20address=20argus=20review=20?= =?UTF-8?q?=E2=80=94=20#=20Errors=20doc,=20regen=20hivectl-cli.md,=20GET?= =?UTF-8?q?=20/api/github-account=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tools/hivectl-cli.md | 32 ++++++++++++++++++++++ hive-c0re/src/dashboard/matrix_accounts.rs | 28 +++++++++++++++++++ hive-c0re/src/dashboard/mod.rs | 2 +- hive-c0re/src/priv_client.rs | 6 ++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 4e685019..d46f5bfd 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -13,6 +13,8 @@ This document contains the help content for the `hivectl` command-line program. * [`hivectl matrix promote-user`↴](#hivectl-matrix-promote-user) * [`hivectl matrix reset-password`↴](#hivectl-matrix-reset-password) * [`hivectl matrix invite`↴](#hivectl-matrix-invite) +* [`hivectl github`↴](#hivectl-github) +* [`hivectl github set-token`↴](#hivectl-github-set-token) * [`hivectl gateway`↴](#hivectl-gateway) * [`hivectl gateway create-user`↴](#hivectl-gateway-create-user) * [`hivectl gateway delete-user`↴](#hivectl-gateway-delete-user) @@ -49,6 +51,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that * `forge` — Forgejo user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`forge::ensure_all`) — useful for recovery, ad-hoc reprovisioning, or single-agent fixes without bouncing the daemon * `matrix` — matrix-tuwunel user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`matrix::ensure_all`) — useful when the boot-time sweep skipped an agent (e.g. matrix container wasn't up yet) or to re-register after wiping a token file +* `github` — 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 * `gateway` — 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 `BCrypt` hashes — no extra service or PAM required * `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket) * `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`) @@ -194,6 +197,35 @@ Invite a matrix user to the hive Space (default) or a specific room. Uses the hi +## `hivectl github` + +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 + +**Usage:** `hivectl github ` + +###### **Subcommands:** + +* `set-token` — 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 + + + +## `hivectl github set-token` + +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 + +**Usage:** `hivectl github set-token [OPTIONS] ` + +###### **Arguments:** + +* `` — Logical agent name (the container/agent name) + +###### **Options:** + +* `--token ` — The PAT value inline. Mutually exclusive with `--token-stdin` +* `--token-stdin` — Read the PAT from stdin (trailing newline stripped). Mutually exclusive with `--token` + + + ## `hivectl gateway` 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 `BCrypt` hashes — no extra service or PAM required diff --git a/hive-c0re/src/dashboard/matrix_accounts.rs b/hive-c0re/src/dashboard/matrix_accounts.rs index e526d7fd..4f5f5c52 100644 --- a/hive-c0re/src/dashboard/matrix_accounts.rs +++ b/hive-c0re/src/dashboard/matrix_accounts.rs @@ -301,6 +301,34 @@ pub(super) async fn post_github_account(Form(f): Form) -> Res axum::Json(GithubAccountResult { ok: true }).into_response() } +#[derive(Deserialize)] +pub(super) struct GithubAccountQuery { + agent: String, +} + +#[derive(Serialize)] +struct GithubAccountStatus { + /// A `github-token` file exists in the agent's state dir (a PAT has been + /// provisioned). A static PAT has no live/heartbeat concept, so this is + /// the only status the credentials tab needs. + present: bool, +} + +/// `GET /api/github-account?agent=` — whether the agent has a GitHub +/// PAT provisioned (its `github-token` file exists). Lets the credentials tab +/// show "token stored" vs "not set" instead of a black-hole paste field. +/// Never returns the token itself. +pub(super) async fn get_github_account(Query(q): Query) -> Response { + let agent = q.agent.trim(); + if !is_plain_ident(agent) { + return error_response(&format!("github-account: invalid agent {agent:?}")); + } + let present = Coordinator::agent_notes_dir(agent) + .join("github-token") + .exists(); + axum::Json(GithubAccountStatus { present }).into_response() +} + /// POST `m.login.password` to `/_matrix/client/v3/login`. /// Returns `(access_token, user_id)`. async fn matrix_password_login( diff --git a/hive-c0re/src/dashboard/mod.rs b/hive-c0re/src/dashboard/mod.rs index b3909047..60facd1f 100644 --- a/hive-c0re/src/dashboard/mod.rs +++ b/hive-c0re/src/dashboard/mod.rs @@ -190,7 +190,7 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> { ) .route( "/api/github-account", - post(matrix_accounts::post_github_account), + post(matrix_accounts::post_github_account).get(matrix_accounts::get_github_account), ) .route( "/api/cancel-reminder/{id}", diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index 4f2d8b31..bd7fb1f1 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -286,6 +286,12 @@ pub async fn write_agent_matrix_token( /// 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`). +/// +/// # Errors +/// +/// Returns an error if the hive-priv call fails — the socket is unreachable, +/// `agent_name` is rejected by the root-side validation, or the file +/// write/chown fails. pub async fn write_agent_github_token(agent_name: &str, token: &str) -> Result<()> { ok(call(&PrivRequest::WriteAgentGithubToken { agent_name: agent_name.to_owned(), From af9b46e80a5c0b870aa51fb8579dd25622082eb3 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 11:29:52 +0200 Subject: [PATCH 07/10] refactor(#1970): replace githubAccount option with hyperhive.github.enable gate (default true, github.com + x-access-token) --- nix/templates/harness-base.nix | 130 +++++++++++---------------------- 1 file changed, 42 insertions(+), 88 deletions(-) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 4825cb5e..19489046 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -17,12 +17,12 @@ let # from `userName` to keep them coupled. userName = config.hyperhive.user.name; homeDir = "/home/${userName}"; - # GitHub account (hyperhive.githubAccount): a `gh` wrapper + a git + # GitHub integration (hyperhive.github.enable): a `gh` wrapper + a git # credential helper, both sourcing the PAT from HIVE_GITHUB_TOKEN_FILE at # invocation time so a dashboard-pasted token takes effect with no rebuild. - # The scripts are static (they read env at runtime) — the account option - # only gates whether they're installed + the env is set, so the token - # value never enters the nix store. + # The scripts are static (they read env at runtime) — the enable flag only + # gates whether they're installed + the env is set, so the token value never + # enters the nix store. github.com only; git auths as `x-access-token` + PAT. ghWrapper = pkgs.writeShellScriptBin "gh" '' if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then GH_TOKEN="$(cat "''${HIVE_GITHUB_TOKEN_FILE}")" @@ -34,7 +34,9 @@ let # git credential-helper protocol: only the `get` action needs an answer. [ "''${1:-}" = "get" ] || exit 0 if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then - printf 'username=%s\n' "''${HIVE_GITHUB_USER:-x-access-token}" + # GitHub ignores the username for PAT auth — `x-access-token` is the + # conventional placeholder; the PAT is the password. + printf 'username=x-access-token\n' printf 'password=%s\n' "$(cat "''${HIVE_GITHUB_TOKEN_FILE}")" fi ''; @@ -645,73 +647,28 @@ in ''; }; - options.hyperhive.githubAccount = lib.mkOption { - type = lib.types.nullOr ( - lib.types.submodule { - options = { - username = lib.mkOption { - type = lib.types.str; - example = "the-sword-above"; - description = '' - GitHub login the agent acts as. Used as the username for - `git push` over HTTPS and shown to the agent via the - `HIVE_GITHUB_USER` environment variable. Should be a - dedicated bot account, never a human's. - ''; - }; - tokenFile = lib.mkOption { - type = lib.types.str; - example = "/agents/damocles/state/github-token"; - description = '' - Path to the file holding this account's personal access - token (PAT). The token *value* is never in nix --- the - provisioner (dashboard credentials tab, or `hivectl`) - writes an operator-supplied PAT here (0600, agent-owned), - the same contract as `matrixAccounts..tokenFile`. - The `gh` wrapper and the git credential helper read the - token from this path at invocation time, so a PAT pasted - mid-session takes effect with no rebuild. `gh` / `git push` - simply fail unauthenticated until the file exists. - - Keep the PAT minimally scoped (only the repos/scopes the - agent's workflow needs): the agent has passwordless sudo, - so a compromised agent can act as the account within the - token's scopes --- scope is the real blast-radius limiter. - ''; - }; - host = lib.mkOption { - type = lib.types.str; - default = "github.com"; - example = "github.example.com"; - description = '' - GitHub host. Defaults to `github.com`; set it for a GitHub - Enterprise instance. Drives both the `gh` API host - (`GH_HOST`) and the git credential helper's URL match. - ''; - }; - }; - } - ); - default = null; - example = lib.literalExpression '' - { - username = "the-sword-above"; - tokenFile = "/agents/damocles/state/github-token"; - } - ''; + options.hyperhive.github.enable = lib.mkOption { + type = lib.types.bool; + default = true; description = '' - Give the agent a managed GitHub account: a `gh` CLI wrapper and a - git credential helper, both authenticated from an operator-supplied - PAT, so the agent can run `gh` API calls and `git push` to GitHub as - the configured login without any manual `gh auth login` dance. + Install the GitHub integration in this agent: a `gh` CLI wrapper and a + git credential helper for `https://github.com`, both authenticated from + an operator-supplied personal access token (PAT). The PAT is written to + `/github-token` out of band --- the dashboard credentials tab or + `hivectl github set-token` --- so giving an agent GitHub is a runtime + paste, no per-agent config or rebuild. The wrappers read the token file + at invocation, so a freshly-pasted PAT takes effect immediately; until + one exists, `gh` / `git push` just fail unauthenticated. - `null` (the default) leaves GitHub off entirely --- no `gh` wrapper, - no credential helper, no env. When set, the token is supplied out of - band (dashboard credentials tab / `hivectl`) into `tokenFile`; nix - only carries the login + host, never the secret. + github.com only. git authenticates as `x-access-token` + the PAT (GitHub + ignores the username for PAT auth); `gh` derives its identity from the + token. Keep the PAT minimally scoped: the agent has passwordless sudo, so + a compromised agent can act within the token's scopes --- scope is the + real blast-radius limiter. - Single account per agent by design (unlike `matrixAccounts`, which is - multi-account): the GitHub workflow is "this agent is this one bot". + On by default. Host-driven: set `services.hyperhive.github.enable = false` + to turn the integration off hive-wide (meta.rs propagates the override + into every agent). ''; }; @@ -1579,14 +1536,11 @@ in # regardless of which profile files are sourced. NIX_REMOTE = "daemon"; } - // lib.optionalAttrs (config.hyperhive.githubAccount != null) { - # GitHub account: metadata + token-file PATH only, never the secret. - # The `gh` wrapper + git credential helper read the PAT from the file - # at invocation time (see hyperhive.githubAccount). - HIVE_GITHUB_USER = config.hyperhive.githubAccount.username; - HIVE_GITHUB_HOST = config.hyperhive.githubAccount.host; - HIVE_GITHUB_TOKEN_FILE = config.hyperhive.githubAccount.tokenFile; - GH_HOST = config.hyperhive.githubAccount.host; + // lib.optionalAttrs config.hyperhive.github.enable { + # GitHub integration: point the gh wrapper + git credential helper at the + # agent's PAT file (written by the credentials tab / `hivectl github + # set-token`). Only the path — never the secret (see hyperhive.github.enable). + HIVE_GITHUB_TOKEN_FILE = "/agents/${userName}/state/github-token"; } // lib.optionalAttrs (!config.hyperhive.autoCompact) { # Zero watermark disables proactive compaction; the reactive path @@ -1705,25 +1659,25 @@ in # (view, pr, issue, comment, assign, close, labels, branches, etc.) (pkgs.callPackage ../packages/hive-forge-tools.nix { }) ] - ++ lib.optionals (config.hyperhive.githubAccount != null) [ - # gh wrapper + git credential helper for hyperhive.githubAccount. + ++ lib.optionals config.hyperhive.github.enable [ + # gh wrapper + git credential helper for hyperhive.github.enable. # (No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the real # binary path, so it can't be shadowed.) ghWrapper gitCredHelper ]; - # Wire the GitHub credential helper for `git push` over HTTPS to the - # configured host. Host-scoped (github.com or a GHE host), so it never - # touches the forge (localhost:3000) or any other remote. Gated on the - # account; the helper reads the PAT from HIVE_GITHUB_TOKEN_FILE at - # invocation (see hyperhive.githubAccount). System /etc/gitconfig merges - # under the agent's ~/.gitconfig (safe.directory), so this is additive. - environment.etc = lib.optionalAttrs (config.hyperhive.githubAccount != null) { + # Wire the GitHub credential helper for `git push` over HTTPS. Host-scoped + # to `https://github.com`, so it never touches the forge (localhost:3000) + # or any other remote. Gated on hyperhive.github.enable; the helper reads + # the PAT from HIVE_GITHUB_TOKEN_FILE at invocation and auths as + # `x-access-token` + the PAT. System /etc/gitconfig merges under the agent's + # ~/.gitconfig (safe.directory), so this is additive. + environment.etc = lib.optionalAttrs config.hyperhive.github.enable { "gitconfig".text = '' - [credential "https://${config.hyperhive.githubAccount.host}"] + [credential "https://github.com"] helper = hive-github - username = ${config.hyperhive.githubAccount.username} + username = x-access-token ''; }; From 18965ff7bd6bef3d620cc85d2656eb1439f3b9b8 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 11:34:57 +0200 Subject: [PATCH 08/10] feat(#1970): host services.hyperhive.github.enable (default true) + meta.rs propagation of the off-switch --- hive-c0re/src/meta.rs | 48 +++++++++++++++++++++++++++++++++++++++ nix/modules/hive-c0re.nix | 22 ++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index a7d833f3..6ae393f2 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -1041,6 +1041,14 @@ where out.push_str(" hyperhive.otel.debug = true;\n"); } } + // GitHub integration is on by default in every agent + // (`hyperhive.github.enable`); the host turns it off hive-wide via + // `services.hyperhive.github.enable = false`, surfaced here as the + // `HYPERHIVE_GITHUB_DISABLED` env on hive-c0re's unit. Only the OFF + // override is propagated — the enabled default needs no per-agent line. + if std::env::var_os("HYPERHIVE_GITHUB_DISABLED").is_some() { + out.push_str(" hyperhive.github.enable = false;\n"); + } out.push_str( r#" # The harness service inside the container runs as a # non-root unix user named after the agent (`damocles`, @@ -1745,4 +1753,44 @@ mod tests { "no otel lines when disabled:\n{off}" ); } + + #[test] + fn render_flake_injects_github_disable_only_when_signalled() { + // services.hyperhive.github.enable = false -> HYPERHIVE_GITHUB_DISABLED + // on hive-c0re's unit -> `hyperhive.github.enable = false` injected into + // every agent. On by default, so nothing is emitted unless disabled. + // + // SAFETY: single-threaded mutation of an env var no other test asserts + // on; restored before returning. + let render = || { + render_flake( + "github:example/hyperhive", + "path:/nix/store/bbbb-hyperhive-docs-source", + "path:/nix/store/aaaa-nixpkgs-source", + 8000, + "she/her", + &std::collections::HashMap::new(), + &[sample_spec("alice", false, 9001)], + ) + }; + unsafe { + std::env::remove_var("HYPERHIVE_GITHUB_DISABLED"); + } + let on_default = render(); + unsafe { + std::env::set_var("HYPERHIVE_GITHUB_DISABLED", "1"); + } + let disabled = render(); + unsafe { + std::env::remove_var("HYPERHIVE_GITHUB_DISABLED"); + } + assert!( + !on_default.contains("hyperhive.github.enable"), + "github.enable must not be emitted by default (agents keep the true default):\n{on_default}" + ); + assert!( + disabled.contains("hyperhive.github.enable = false;"), + "github.enable = false must be injected when the host disables it:\n{disabled}" + ); + } } diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 184c63a8..0bdaaa5c 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -215,6 +215,22 @@ in ''; }; + options.services.hyperhive.github.enable = lib.mkOption { + type = lib.types.bool; + default = true; + example = false; + description = '' + Hive-wide switch for the per-agent GitHub integration (the `gh` CLI + wrapper + git credential helper, per `hyperhive.github.enable`). On by + default: every agent gets the integration, inert until a PAT is + provisioned via the dashboard credentials tab or `hivectl github + set-token`. Set `false` to turn it off for the whole hive --- the + meta-flake renderer (`hive-c0re/src/meta.rs`) then injects + `hyperhive.github.enable = false` into every agent. Exposed to hive-c0re + as `HYPERHIVE_GITHUB_DISABLED` (set only when the integration is off). + ''; + }; + # Hive-wide OTEL stats export. Set ONCE here at host level; the # meta-flake renderer (`hive-c0re/src/meta.rs::otel_config`) reads the # HYPERHIVE_OTEL_* env exported below off hive-c0re's unit and injects @@ -956,6 +972,12 @@ in // lib.optionalAttrs (config.services.hyperhive.swarmName != null) { HYPERHIVE_SWARM_NAME = config.services.hyperhive.swarmName; } + // lib.optionalAttrs (!config.services.hyperhive.github.enable) { + # GitHub integration is on by default; only signal the OFF override to + # meta.rs, which then injects `hyperhive.github.enable = false` into + # every agent. See services.hyperhive.github.enable. + HYPERHIVE_GITHUB_DISABLED = "1"; + } // lib.optionalAttrs config.services.hyperhive.otel.enable ( # Hive-wide OTEL config -> read by meta.rs::otel_config and # injected as build-time `hyperhive.otel.*` into every agent. From cef9e633f7377d44e83358b4fc96b9925b9dfa54 Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 11:39:06 +0200 Subject: [PATCH 09/10] docs(#1970): rewrite for UI-driven shape (github.enable + host switch), purge githubAccount refs, regen hivectl-cli.md --- README.md | 12 +++---- docs/github.md | 63 ++++++++++++++++++++---------------- docs/tools/hivectl-cli.md | 8 ++--- docs/tools/hivectl.md | 7 ++-- hive-c0re/src/bin/hivectl.rs | 18 ++++++----- hive-c0re/src/priv_client.rs | 2 +- hive-sh4re/src/priv_proto.rs | 3 +- 7 files changed, 61 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index ca99fdac..0a9e8e7d 100644 --- a/README.md +++ b/README.md @@ -120,17 +120,15 @@ For more details see [`docs/matrix.md`](docs/matrix.md). ### GitHub account -`hyperhive.githubAccount` gives an agent a managed GitHub identity — a `gh` CLI wrapper and `git push` over HTTPS, both authenticated as a configured bot account: +Every agent gets a managed GitHub identity — a `gh` CLI wrapper and `git push` over HTTPS — on by default (`hyperhive.github.enable`), inert until a PAT is provisioned. There is nothing per-agent to declare: paste an operator-supplied personal access token into the agent's dashboard **credentials** tab (or `hivectl github set-token --token-stdin`) and it works. The `gh` wrapper + git credential helper read the token live (git auths as `x-access-token` + the PAT; github.com only), so a rotated PAT takes effect with no rebuild. + +Turn the integration off for the whole hive with the host option: ```nix -hyperhive.githubAccount = { - username = "the-sword-above"; - tokenFile = "/agents/myagent/state/github-token"; # PAT provisioned out-of-band - # host = "github.com"; # default; set for GHE -}; +services.hyperhive.github.enable = false; ``` -The PAT value is never in nix; write it with `hivectl github set-token --token-stdin`. Both the `gh` wrapper and the git credential helper read the token live, so a rotated PAT takes effect with no rebuild. Single account per agent. For more details see [`docs/github.md`](docs/github.md). +The PAT value is never in nix — only the enable flag. For more details see [`docs/github.md`](docs/github.md). ## Operator CLI diff --git a/docs/github.md b/docs/github.md index 3212acf1..8665f8fb 100644 --- a/docs/github.md +++ b/docs/github.md @@ -1,47 +1,51 @@ # GitHub accounts Give an agent a managed GitHub identity — a `gh` CLI and `git push` over -HTTPS, both authenticated as a configured bot account — so it can run -GitHub API calls and push commits without any manual `gh auth login`. +HTTPS, both authenticated by an operator-supplied personal access token +(PAT) — so it can run GitHub API calls and push commits without any manual +`gh auth login`. -This mirrors the [matrix account](matrix.md) pattern: nix carries the -login + host, never the secret; an operator-supplied personal access -token (PAT) is injected out of band into the agent's state dir. +Provisioning is UI-driven, mirroring the dashboard side of the +[matrix account](matrix.md) flow: paste a PAT into the agent's credentials +tab and it works. No per-agent nix declaration, no rebuild — the token is +injected into the agent's state dir out of band. -## Config option +## Enabling -Declare `hyperhive.githubAccount` in the agent's `agent.nix`: +The integration is **on by default** for every agent (`hyperhive.github.enable += true`), inert until a PAT is provisioned. There is nothing per-agent to +declare — an agent gains GitHub simply by having a PAT written to its token +file. + +To turn it off for the whole hive, set the host option: ```nix -hyperhive.githubAccount = { - username = "the-sword-above"; # the bot login - tokenFile = "/agents//state/github-token"; # where the PAT lives - # host = "github.com"; # default; set for GHE -}; +services.hyperhive.github.enable = false; ``` -`null` (the default) leaves GitHub off entirely — no `gh` wrapper, no -credential helper, no env. Single account per agent by design (unlike -`matrixAccounts`, which is multi-account): the workflow is "this agent is -this one bot". +hive-c0re's meta-flake renderer then injects `hyperhive.github.enable = false` +into every agent, so no agent ships the `gh` wrapper or credential helper. +(`hyperhive.github.enable` also exists per-agent for completeness, but the +hive-wide host switch is the intended control.) -The token **value** is never in nix. `tokenFile` only names the path; the -PAT is written there separately (see [Provisioning](#provisioning)). +github.com only. The token **value** never touches nix — it is written to +`/github-token` separately (see [Provisioning](#provisioning)). ## How the agent uses it -When `githubAccount` is set, the container gets: +When enabled, the container gets: - **A `gh` wrapper** on `PATH` (shadowing the raw `gh`) that exports `GH_TOKEN` from the token file at invocation, then execs real `gh`. So - `gh pr create`, `gh api …`, etc. just work as the bot. + `gh pr create`, `gh api …`, etc. just work — `gh` derives the identity + from the token. - **A git credential helper** (`git-credential-hive-github`), wired via a - host-scoped `/etc/gitconfig` entry for `https://`, so - `git push https://github.com//` authenticates as the bot. + host-scoped `/etc/gitconfig` entry for `https://github.com`, so + `git push https://github.com//` authenticates as + `x-access-token` + the PAT (GitHub ignores the username for PAT auth). Host-scoped, so it never touches the forge (`localhost:3000`) or any other remote. -- **Env**: `HIVE_GITHUB_USER`, `HIVE_GITHUB_HOST`, - `HIVE_GITHUB_TOKEN_FILE`, and `GH_HOST`. +- **Env**: `HIVE_GITHUB_TOKEN_FILE` (the token path — never the secret). Both the wrapper and the credential helper read the token from the file **at invocation time**, so a PAT written (or rotated) mid-session takes @@ -50,16 +54,19 @@ exists, `gh` / `git push` simply fail unauthenticated. ## Provisioning -The PAT is operator-supplied. Write it into the agent's token file with: +The PAT is operator-supplied. The primary path is the **dashboard +credentials tab** (github sub-tab): paste the PAT for an agent and submit +(`POST /api/github-account`). There is also a CLI path for +recovery/scripting: ```sh hivectl github set-token --token-stdin # paste the PAT on stdin (preferred) hivectl github set-token --token # inline (visible in shell history) ``` -hive-c0re delegates the write to hive-priv, which stores the file `0600` -owned by the agent (so the container can read it) — the same credential -injection path as forge/matrix tokens. See +Either path has hive-c0re delegate the write to hive-priv, which stores the +file `0600` owned by the agent (so the container can read it) — the same +credential-injection path as forge/matrix tokens. See [hivectl → GitHub](tools/hivectl.md#github). ## Security diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index d46f5bfd..6c5b5d34 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -51,7 +51,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that * `forge` — Forgejo user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`forge::ensure_all`) — useful for recovery, ad-hoc reprovisioning, or single-agent fixes without bouncing the daemon * `matrix` — matrix-tuwunel user provisioning. Manual entry point to the same idempotent flow c0re runs automatically at boot (`matrix::ensure_all`) — useful when the boot-time sweep skipped an agent (e.g. matrix container wasn't up yet) or to re-register after wiping a token file -* `github` — 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` — GitHub account provisioning: write an operator-supplied personal access token (PAT) into an agent's `github-token` state 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. A CLI alternative to the dashboard credentials tab; the integration is on by default (`hyperhive.github.enable`), so no per-agent config is needed * `gateway` — 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 `BCrypt` hashes — no extra service or PAM required * `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket) * `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`) @@ -199,19 +199,19 @@ Invite a matrix user to the hive Space (default) or a specific room. Uses the hi ## `hivectl github` -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 account provisioning: write an operator-supplied personal access token (PAT) into an agent's `github-token` state 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. A CLI alternative to the dashboard credentials tab; the integration is on by default (`hyperhive.github.enable`), so no per-agent config is needed **Usage:** `hivectl github ` ###### **Subcommands:** -* `set-token` — 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 +* `set-token` — Write a GitHub PAT into ``'s state dir (`github-token`, 0600, agent-owned) via hive-priv. The GitHub integration is on by default (`hyperhive.github.enable`), so the `gh` wrapper + git credential helper pick the token up with no per-agent config. 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 ## `hivectl github set-token` -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 +Write a GitHub PAT into ``'s state dir (`github-token`, 0600, agent-owned) via hive-priv. The GitHub integration is on by default (`hyperhive.github.enable`), so the `gh` wrapper + git credential helper pick the token up with no per-agent config. 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 **Usage:** `hivectl github set-token [OPTIONS] ` diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 38d1c84f..04c208cc 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -79,9 +79,10 @@ hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a spec Write an operator-supplied GitHub personal access token (PAT) into an agent's token file so its `gh` wrapper + git credential helper can act as -the configured bot account. Unlike forge/matrix there is no account -creation — the PAT is for an existing GitHub account. The agent must -declare [`hyperhive.githubAccount`](../github.md). +the bot account. Unlike forge/matrix there is no account creation — the PAT +is for an existing GitHub account. A CLI alternative to the dashboard +credentials tab; the [GitHub integration](../github.md) is on by default +(`hyperhive.github.enable`), so no per-agent config is needed. ```bash hivectl github set-token damocles --token-stdin # paste the PAT on stdin (preferred) diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 3d0d69ef..fa0a0540 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -71,10 +71,12 @@ enum Cmd { 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. + /// access token (PAT) into an agent's `github-token` state 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. A CLI alternative to the dashboard + /// credentials tab; the integration is on by default + /// (`hyperhive.github.enable`), so no per-agent config is needed. Github { #[command(subcommand)] cmd: GithubCmd, @@ -433,10 +435,10 @@ 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 + /// agent-owned) via hive-priv. The GitHub integration is on by default + /// (`hyperhive.github.enable`), so the `gh` wrapper + git credential + /// helper pick the token up with no per-agent config. 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 { diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs index bd7fb1f1..9b69ee68 100644 --- a/hive-c0re/src/priv_client.rs +++ b/hive-c0re/src/priv_client.rs @@ -285,7 +285,7 @@ pub async fn write_agent_matrix_token( /// (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`). +/// operator-supplied (for the agent's GitHub integration, `hyperhive.github.enable`). /// /// # Errors /// diff --git a/hive-sh4re/src/priv_proto.rs b/hive-sh4re/src/priv_proto.rs index 71dd4929..bb4361e4 100644 --- a/hive-sh4re/src/priv_proto.rs +++ b/hive-sh4re/src/priv_proto.rs @@ -414,7 +414,8 @@ pub enum PrivRequest { /// 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 + /// agent's GitHub integration (`hyperhive.github.enable`). 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 From bd0e554447d25d3856866d32a7bac75b1649eb3f Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 11:42:59 +0200 Subject: [PATCH 10/10] =?UTF-8?q?fix(#1970):=20nested-path=20environment.e?= =?UTF-8?q?tc."gitconfig"=20+=20mkIf=20=E2=80=94=20whole-set=20binding=20c?= =?UTF-8?q?ollided=20with=20sibling=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nix/templates/harness-base.nix | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 19489046..13eefd3b 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -1673,8 +1673,11 @@ in # the PAT from HIVE_GITHUB_TOKEN_FILE at invocation and auths as # `x-access-token` + the PAT. System /etc/gitconfig merges under the agent's # ~/.gitconfig (safe.directory), so this is additive. - environment.etc = lib.optionalAttrs config.hyperhive.github.enable { - "gitconfig".text = '' + # Nested-path binding + mkIf (matching the other `environment.etc."…"` + # entries above) — a whole-set `environment.etc = {…}` here would collide + # with them at the nix level ("attribute already defined"). + environment.etc."gitconfig" = lib.mkIf config.hyperhive.github.enable { + text = '' [credential "https://github.com"] helper = hive-github username = x-access-token