From 5fb4b9f4b02cac8f97fff82bcd19176a5e7618af Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 10:37:18 +0200 Subject: [PATCH] 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):