refactor(#1970): replace githubAccount option with hyperhive.github.enable gate (default true, github.com + x-access-token)

This commit is contained in:
damocles 2026-07-11 11:29:52 +02:00 committed by mara
commit af9b46e80a

View file

@ -17,12 +17,12 @@ let
# from `userName` to keep them coupled. # from `userName` to keep them coupled.
userName = config.hyperhive.user.name; userName = config.hyperhive.user.name;
homeDir = "/home/${userName}"; 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 # credential helper, both sourcing the PAT from HIVE_GITHUB_TOKEN_FILE at
# invocation time so a dashboard-pasted token takes effect with no rebuild. # invocation time so a dashboard-pasted token takes effect with no rebuild.
# The scripts are static (they read env at runtime) — the account option # The scripts are static (they read env at runtime) — the enable flag only
# only gates whether they're installed + the env is set, so the token # gates whether they're installed + the env is set, so the token value never
# value never enters the nix store. # enters the nix store. github.com only; git auths as `x-access-token` + PAT.
ghWrapper = pkgs.writeShellScriptBin "gh" '' ghWrapper = pkgs.writeShellScriptBin "gh" ''
if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then
GH_TOKEN="$(cat "''${HIVE_GITHUB_TOKEN_FILE}")" GH_TOKEN="$(cat "''${HIVE_GITHUB_TOKEN_FILE}")"
@ -34,7 +34,9 @@ let
# git credential-helper protocol: only the `get` action needs an answer. # git credential-helper protocol: only the `get` action needs an answer.
[ "''${1:-}" = "get" ] || exit 0 [ "''${1:-}" = "get" ] || exit 0
if [ -n "''${HIVE_GITHUB_TOKEN_FILE:-}" ] && [ -r "''${HIVE_GITHUB_TOKEN_FILE}" ]; then 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}")" printf 'password=%s\n' "$(cat "''${HIVE_GITHUB_TOKEN_FILE}")"
fi fi
''; '';
@ -645,73 +647,28 @@ in
''; '';
}; };
options.hyperhive.githubAccount = lib.mkOption { options.hyperhive.github.enable = lib.mkOption {
type = lib.types.nullOr ( type = lib.types.bool;
lib.types.submodule { default = true;
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.<name>.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 = '' description = ''
Give the agent a managed GitHub account: a `gh` CLI wrapper and a Install the GitHub integration in this agent: a `gh` CLI wrapper and a
git credential helper, both authenticated from an operator-supplied git credential helper for `https://github.com`, both authenticated from
PAT, so the agent can run `gh` API calls and `git push` to GitHub as an operator-supplied personal access token (PAT). The PAT is written to
the configured login without any manual `gh auth login` dance. `<state>/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, github.com only. git authenticates as `x-access-token` + the PAT (GitHub
no credential helper, no env. When set, the token is supplied out of ignores the username for PAT auth); `gh` derives its identity from the
band (dashboard credentials tab / `hivectl`) into `tokenFile`; nix token. Keep the PAT minimally scoped: the agent has passwordless sudo, so
only carries the login + host, never the secret. 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 On by default. Host-driven: set `services.hyperhive.github.enable = false`
multi-account): the GitHub workflow is "this agent is this one bot". 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. # regardless of which profile files are sourced.
NIX_REMOTE = "daemon"; NIX_REMOTE = "daemon";
} }
// lib.optionalAttrs (config.hyperhive.githubAccount != null) { // lib.optionalAttrs config.hyperhive.github.enable {
# GitHub account: metadata + token-file PATH only, never the secret. # GitHub integration: point the gh wrapper + git credential helper at the
# The `gh` wrapper + git credential helper read the PAT from the file # agent's PAT file (written by the credentials tab / `hivectl github
# at invocation time (see hyperhive.githubAccount). # set-token`). Only the path — never the secret (see hyperhive.github.enable).
HIVE_GITHUB_USER = config.hyperhive.githubAccount.username; HIVE_GITHUB_TOKEN_FILE = "/agents/${userName}/state/github-token";
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) { // lib.optionalAttrs (!config.hyperhive.autoCompact) {
# Zero watermark disables proactive compaction; the reactive path # Zero watermark disables proactive compaction; the reactive path
@ -1705,25 +1659,25 @@ in
# (view, pr, issue, comment, assign, close, labels, branches, etc.) # (view, pr, issue, comment, assign, close, labels, branches, etc.)
(pkgs.callPackage ../packages/hive-forge-tools.nix { }) (pkgs.callPackage ../packages/hive-forge-tools.nix { })
] ]
++ lib.optionals (config.hyperhive.githubAccount != null) [ ++ lib.optionals config.hyperhive.github.enable [
# gh wrapper + git credential helper for hyperhive.githubAccount. # gh wrapper + git credential helper for hyperhive.github.enable.
# (No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the real # (No bare pkgs.gh here — the wrapper *is* `gh` and hardcodes the real
# binary path, so it can't be shadowed.) # binary path, so it can't be shadowed.)
ghWrapper ghWrapper
gitCredHelper gitCredHelper
]; ];
# Wire the GitHub credential helper for `git push` over HTTPS to the # Wire the GitHub credential helper for `git push` over HTTPS. Host-scoped
# configured host. Host-scoped (github.com or a GHE host), so it never # to `https://github.com`, so it never touches the forge (localhost:3000)
# touches the forge (localhost:3000) or any other remote. Gated on the # or any other remote. Gated on hyperhive.github.enable; the helper reads
# account; the helper reads the PAT from HIVE_GITHUB_TOKEN_FILE at # the PAT from HIVE_GITHUB_TOKEN_FILE at invocation and auths as
# invocation (see hyperhive.githubAccount). System /etc/gitconfig merges # `x-access-token` + the PAT. System /etc/gitconfig merges under the agent's
# under the agent's ~/.gitconfig (safe.directory), so this is additive. # ~/.gitconfig (safe.directory), so this is additive.
environment.etc = lib.optionalAttrs (config.hyperhive.githubAccount != null) { environment.etc = lib.optionalAttrs config.hyperhive.github.enable {
"gitconfig".text = '' "gitconfig".text = ''
[credential "https://${config.hyperhive.githubAccount.host}"] [credential "https://github.com"]
helper = hive-github helper = hive-github
username = ${config.hyperhive.githubAccount.username} username = x-access-token
''; '';
}; };