diff --git a/nix/packages/hive-forge-tools.nix b/nix/packages/hive-forge-tools.nix deleted file mode 100644 index b5e40a35..00000000 --- a/nix/packages/hive-forge-tools.nix +++ /dev/null @@ -1,250 +0,0 @@ -{ pkgs, lib }: -# Single `hive-forge ` CLI wrapping common Forgejo API operations. -# Reads credentials from the environment: -# -# HIVE_FORGE_URL -- base URL, e.g. http://localhost:3000 -# HIVE_FORGE_REPO -- default repo, e.g. hyperhive/hyperhive -# HYPERHIVE_STATE_DIR -- state dir; forge-token lives here -# -# Usage: hive-forge [args...] -# Verbs: view, issue, pr, comment, assign, close, labels, pr-reviews, -# branches, tree-sha -pkgs.writeShellApplication { - name = "hive-forge"; - runtimeInputs = [ - pkgs.curl - pkgs.jq - ]; - text = '' - : "''${HIVE_FORGE_URL:=http://localhost:3000}" - : "''${HIVE_FORGE_REPO:=hyperhive/hyperhive}" - _state_dir="''${HYPERHIVE_STATE_DIR:-}" - _token_file="''${_state_dir:+$_state_dir/}forge-token" - if [ ! -f "$_token_file" ]; then - echo "hive-forge: no forge-token at $_token_file" >&2 - exit 1 - fi - _token=$(cat "$_token_file") - FORGE_API="$HIVE_FORGE_URL/api/v1" - - forge_get() { - ${pkgs.curl}/bin/curl -sf \ - -H "Authorization: token $_token" \ - -H "Accept: application/json" \ - "$1" - } - forge_post() { - ${pkgs.curl}/bin/curl -sf -X POST \ - -H "Authorization: token $_token" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -d "$2" "$1" - } - forge_patch() { - ${pkgs.curl}/bin/curl -sf -X PATCH \ - -H "Authorization: token $_token" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -d "$2" "$1" - } - forge_delete() { - local _url="$1" - local _body="''${2:-}" - if [ -n "$_body" ]; then - ${pkgs.curl}/bin/curl -sf -X DELETE \ - -H "Authorization: token $_token" \ - -H "Content-Type: application/json" \ - -H "Accept: application/json" \ - -d "$_body" "$_url" - else - ${pkgs.curl}/bin/curl -sf -X DELETE \ - -H "Authorization: token $_token" \ - -H "Accept: application/json" \ - "$_url" - fi - } - - cmd_view() { - # view [repo] - # Dump title + body + all comments for an issue or PR. - if [ $# -lt 1 ]; then echo "usage: hive-forge view [repo]" >&2; exit 1; fi - local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" - local _issue _title _body _state _user _is_pr _kind - _issue=$(forge_get "$FORGE_API/repos/$_repo/issues/$_n") - _title=$(printf '%s' "$_issue" | jq -r '.title') - _body=$(printf '%s' "$_issue" | jq -r '.body // ""') - _state=$(printf '%s' "$_issue" | jq -r '.state') - _user=$(printf '%s' "$_issue" | jq -r '.user.login') - _is_pr=$(printf '%s' "$_issue" | jq -r '.pull_request != null') - _kind="issue" - if [ "$_is_pr" = "true" ]; then _kind="PR"; fi - printf '# %s #%s [%s] by %s\n' "$_kind" "$_n" "$_state" "$_user" - printf '%s\n' "$_title" - if [ -n "$_body" ]; then printf '\n%s\n' "$_body"; fi - local _comments _count - _comments=$(forge_get "$FORGE_API/repos/$_repo/issues/$_n/comments?limit=50") - _count=$(printf '%s' "$_comments" | jq 'length') - if [ "$_count" -gt 0 ]; then - printf '\n---\n## Comments (%s)\n\n' "$_count" - printf '%s' "$_comments" | jq -r '.[] | "**\(.user.login)**: \(.body)\n"' - fi - } - - cmd_issue() { - # issue [repo] - # Print key fields of an issue as JSON. - if [ $# -lt 1 ]; then echo "usage: hive-forge issue [repo]" >&2; exit 1; fi - local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" - forge_get "$FORGE_API/repos/$_repo/issues/$_n" \ - | jq '{number,title,state,user:.user.login,assignees:[.assignees[]?.login],labels:[.labels[]?.name],body}' - } - - cmd_pr() { - # pr [repo] - # Print key fields of a PR as JSON. - if [ $# -lt 1 ]; then echo "usage: hive-forge pr [repo]" >&2; exit 1; fi - local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" - forge_get "$FORGE_API/repos/$_repo/pulls/$_n" \ - | jq '{number,title,state,merged,user:.user.login,head_sha:.head.sha,head_branch:.head.label,base_branch:.base.label}' - } - - cmd_comment() { - # comment [body | -f | - for stdin] - # Post a comment on an issue or PR. - if [ $# -lt 1 ]; then echo "usage: hive-forge comment [body | -f | -]" >&2; exit 1; fi - local _n="$1"; shift - local _body - if [ $# -eq 0 ] || [ "''${1:-}" = "-" ]; then - _body=$(cat) - elif [ "''${1:-}" = "-f" ]; then - _body=$(cat "$2") - else - _body="$*" - fi - local _payload - _payload=$(jq -n --arg body "$_body" '{body:$body}') - forge_post "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/comments" "$_payload" \ - | jq '{id,url:.html_url}' - } - - cmd_assign() { - # assign [--remove] - # Assign or unassign a user on an issue or PR. - if [ $# -lt 2 ]; then echo "usage: hive-forge assign [--remove]" >&2; exit 1; fi - local _n="$1" _user="$2" _remove="''${3:-}" - local _payload - _payload=$(jq -n --arg u "$_user" '{assignees:[$u]}') - if [ "$_remove" = "--remove" ]; then - forge_delete "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/assignees" "$_payload" \ - | jq '{number,assignees:[.assignees[]?.login]}' - else - forge_post "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/assignees" "$_payload" \ - | jq '{number,assignees:[.assignees[]?.login]}' - fi - } - - cmd_close() { - # close - # Close an issue or PR. - if [ $# -lt 1 ]; then echo "usage: hive-forge close " >&2; exit 1; fi - forge_patch "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$1" '{"state":"closed"}' \ - | jq '{number,state}' - } - - cmd_labels() { - # labels [list|add|remove] [labels...] - # Manage labels on an issue or PR. Default action is list. - if [ $# -lt 1 ]; then echo "usage: hive-forge labels [list|add|remove] [labels...]" >&2; exit 1; fi - local _n="$1" _action="''${2:-list}" - shift; shift || true - local _all _ids _id _label - case "$_action" in - list) - forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels" | jq '[.[].name]' - ;; - add) - _all=$(forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/labels?limit=100") - _ids=$(printf '%s' "$_all" | jq -r --args '[.[] | select(.name == ($ARGS.positional[])) | .id]' -- "$@") - forge_post "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels" "{\"labels\":$_ids}" \ - | jq '[.[].name]' - ;; - remove) - _all=$(forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/labels?limit=100") - for _label in "$@"; do - _id=$(printf '%s' "$_all" | jq -r --arg n "$_label" '.[] | select(.name==$n) | .id') - if [ -n "$_id" ]; then - forge_delete "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels/$_id" - fi - done - forge_get "$FORGE_API/repos/$HIVE_FORGE_REPO/issues/$_n/labels" | jq '[.[].name]' - ;; - *) - echo "unknown action: $_action (use list, add, remove)" >&2; exit 1 - ;; - esac - } - - cmd_pr_reviews() { - # pr-reviews [repo] - # List reviews on a PR with id/state/user/body. - if [ $# -lt 1 ]; then echo "usage: hive-forge pr-reviews [repo]" >&2; exit 1; fi - local _n="$1" _repo="''${2:-$HIVE_FORGE_REPO}" - forge_get "$FORGE_API/repos/$_repo/pulls/$_n/reviews" \ - | jq '[.[] | {id,state,user:.user.login,body,comments_count}]' - } - - cmd_branches() { - # branches [pattern] [repo] - # List branches, optionally filtered by a grep pattern. - local _pattern="''${1:-}" _repo="''${2:-$HIVE_FORGE_REPO}" - local _result - _result=$(forge_get "$FORGE_API/repos/$_repo/branches?limit=100" | jq -r '.[].name') - if [ -n "$_pattern" ]; then - printf '%s\n' "$_result" | grep "$_pattern" || true - else - printf '%s\n' "$_result" - fi - } - - cmd_tree_sha() { - # tree-sha [repo] - # Print the tree SHA of the commit at the given branch or commit SHA. - if [ $# -lt 1 ]; then echo "usage: hive-forge tree-sha [repo]" >&2; exit 1; fi - local _ref="$1" _repo="''${2:-$HIVE_FORGE_REPO}" - local _commit_sha - _commit_sha=$(forge_get "$FORGE_API/repos/$_repo/branches/$_ref" 2>/dev/null \ - | jq -r '.commit.id // empty') || true - if [ -z "$_commit_sha" ]; then - _commit_sha="$_ref" - fi - forge_get "$FORGE_API/repos/$_repo/git/commits/$_commit_sha" \ - | jq -r '.tree.sha // .sha' - } - - VERB="''${1:-}" - if [ -z "$VERB" ]; then - echo "usage: hive-forge [args...]" >&2 - echo "verbs: view, issue, pr, comment, assign, close, labels, pr-reviews, branches, tree-sha" >&2 - exit 1 - fi - shift - - case "$VERB" in - view) cmd_view "$@" ;; - issue) cmd_issue "$@" ;; - pr) cmd_pr "$@" ;; - comment) cmd_comment "$@" ;; - assign) cmd_assign "$@" ;; - close) cmd_close "$@" ;; - labels) cmd_labels "$@" ;; - pr-reviews) cmd_pr_reviews "$@" ;; - branches) cmd_branches "$@" ;; - tree-sha) cmd_tree_sha "$@" ;; - *) - echo "hive-forge: unknown verb '$VERB'" >&2 - echo "verbs: view, issue, pr, comment, assign, close, labels, pr-reviews, branches, tree-sha" >&2 - exit 1 - ;; - esac - ''; -} diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index fa339f26..9ec301bd 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -248,10 +248,7 @@ # `nix flake` constantly (devshells, ad-hoc evals, fetching their # own MCP-server flakes); without this they hit the "experimental # feature not enabled" wall on the first try. - nix.settings.experimental-features = [ - "nix-command" - "flakes" - ]; + nix.settings.experimental-features = [ "nix-command" "flakes" ]; # `claude-code` is unfree. Each per-agent container's nixosConfiguration # evaluates its own `nixpkgs` instance, so the operator's host-level @@ -276,9 +273,6 @@ 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 { }) ]; # One-shot: write tea's config.yml from the seeded forge token so @@ -300,57 +294,57 @@ pkgs.coreutils ]; script = '' - # No `set -e`: any subshell failure must not propagate. - # A failed unit aborts `nixos-container update` which blocks rebuilds. - FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url} - # Manager bind-mounts state at /state; sub-agents at - # /agents//state. Glob both — each container only sees - # its own mount, so there is exactly one hit (or zero when - # the forge hasn't been seeded yet). - TOKEN_FILE="" - for f in /state/forge-token /agents/*/state/forge-token; do - if [ -f "$f" ]; then - TOKEN_FILE="$f" - break - fi - done - if [ -z "$TOKEN_FILE" ]; then - echo "tea-login: no forge-token found; skipping" - exit 0 - fi - TOKEN=$(cat "$TOKEN_FILE") - # Resolve the agent username from the forge API. - USER=$(curl -sf --max-time 5 \ - -H "Authorization: token $TOKEN" \ - "$FORGE_URL/api/v1/user" \ - | python3 -c 'import sys,json; print(json.load(sys.stdin).get("login",""))' \ - 2>/dev/null || true) - if [ -z "$USER" ]; then - echo "tea-login: could not resolve username from forge API; skipping" - exit 0 - fi - # tea reads config from $HOME/.config/tea/config.yml. - # Write it directly so we control default:true and always - # refresh a rotated token — no 'tea login add' interactive dance. - CONFIG="$HOME/.config/tea/config.yml" - mkdir -p "$(dirname "$CONFIG")" || true - cat > "$CONFIG" << EOF - logins: - - name: forge - url: $FORGE_URL - token: $TOKEN - default: true - ssh_host: "" - ssh_key: "" - insecure: false - ssh_agent: false - user: $USER - preferences: - editor: false - flag_defaults: - remote: "" - EOF - echo "tea-login: configured for $FORGE_URL as $USER" + # No `set -e`: any subshell failure must not propagate. + # A failed unit aborts `nixos-container update` which blocks rebuilds. + FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url} + # Manager bind-mounts state at /state; sub-agents at + # /agents//state. Glob both — each container only sees + # its own mount, so there is exactly one hit (or zero when + # the forge hasn't been seeded yet). + TOKEN_FILE="" + for f in /state/forge-token /agents/*/state/forge-token; do + if [ -f "$f" ]; then + TOKEN_FILE="$f" + break + fi + done + if [ -z "$TOKEN_FILE" ]; then + echo "tea-login: no forge-token found; skipping" + exit 0 + fi + TOKEN=$(cat "$TOKEN_FILE") + # Resolve the agent username from the forge API. + USER=$(curl -sf --max-time 5 \ + -H "Authorization: token $TOKEN" \ + "$FORGE_URL/api/v1/user" \ + | python3 -c 'import sys,json; print(json.load(sys.stdin).get("login",""))' \ + 2>/dev/null || true) + if [ -z "$USER" ]; then + echo "tea-login: could not resolve username from forge API; skipping" + exit 0 + fi + # tea reads config from $HOME/.config/tea/config.yml. + # Write it directly so we control default:true and always + # refresh a rotated token — no 'tea login add' interactive dance. + CONFIG="$HOME/.config/tea/config.yml" + mkdir -p "$(dirname "$CONFIG")" || true + cat > "$CONFIG" << EOF +logins: + - name: forge + url: $FORGE_URL + token: $TOKEN + default: true + ssh_host: "" + ssh_key: "" + insecure: false + ssh_agent: false + user: $USER +preferences: + editor: false + flag_defaults: + remote: "" +EOF + echo "tea-login: configured for $FORGE_URL as $USER" ''; };