Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e74d45e
🐛 fix(dashboard): keep the sweep's own reason on a blocked PR pill (#…
Danathar Sep 18, 2026
9fb8f47
🔖 release: v4.54.2
actions-user Sep 18, 2026
5413ec5
Merge pull request #7530 from hivecommons/release-gate/v4.54.2
clubanderson Sep 18, 2026
bf29840
[strategist] planning: define the v6 designation before it defines it…
hivecommons-hive[bot] Sep 18, 2026
ffb58e6
✨ feat(review): route blocking verdicts to a human (#7527)
clubanderson Sep 18, 2026
21c6482
✨ feat(review): spend review slots breadth-first on deep queues (#7528)
clubanderson Sep 18, 2026
e96c72c
🔒 security: make the token-access audit log non-writable by agents (b…
hivecommons-hive[bot] Sep 18, 2026
2386969
🐛 fix(hub): reconcile #7457's auth-url onto the hive-contribute Ingre…
Danathar Sep 18, 2026
436a8bd
📖 docs: refresh api-reference citations drifted by the auth-url recon…
clubanderson Sep 18, 2026
8cba385
📖 docs: re-point delegation-keys citation at its HandleFunc line
clubanderson Sep 18, 2026
180ddad
🌱 dashboard: name the branch-protection rule behind a blocked PR pill
clubanderson Sep 18, 2026
ed4edfb
🌱 ci: cap Actions cache segment downloads at 1 minute
clubanderson Sep 18, 2026
7f1871e
refactor(agent): split consent/env/modes/routing/thrash/copilot_auth …
clubanderson Sep 18, 2026
33200bd
🔖 release: v4.55.0
actions-user Sep 18, 2026
24dec9c
Merge pull request #7540 from hivecommons/release-gate/v4.55.0
clubanderson Sep 18, 2026
24a67b0
[quality] make copilot SDK-helper probe tests hermetic (#7535)
hivecommons-hive[bot] Sep 18, 2026
99f2550
🌱 cmd/hive: extract five business-logic domains from main.go (9,795 →…
clubanderson Sep 18, 2026
d5f311d
🐛 fix(dashboard): refresh served status agent block on the fast tick …
clubanderson Sep 18, 2026
26e9773
🔖 release: v4.55.1
actions-user Sep 18, 2026
201c69c
Merge pull request #7542 from hivecommons/release-gate/v4.55.1
clubanderson Sep 18, 2026
aecaaba
🌱 Forward-merge v4 into v5 through 201c69c8a (20 commits)
clubanderson Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/v2-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

# A slow cache-segment download must never cost more than a recompile
# (see v2-tests.yml — observed 16-minute restore steps on some nodes).
env:
ACTIONS_CACHE_SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'

jobs:
build-and-test:
runs-on: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && fromJSON(vars.HIVE_RUNNER_LABELS || '["ubuntu-latest"]') || 'ubuntu-latest' }}
Expand Down Expand Up @@ -151,8 +156,12 @@ jobs:
- name: Entrypoint UID-isolation migration (#5525)
run: bash deploy/test_entrypoint_uid_isolation.sh

# #6287: the token-access audit log must not be agent-writable. The
# wrappers write only to the drop-box spool and no chmod may loosen it.
# #6287: the token-access audit log (GET /api/token-access) was
# dev:node 0664 while every agent UID is in group node, so the audited
# agents could truncate or forge their own trail. Fixed on v5 by #6303
# and backported here by #7523. Pins the invariant on the shipped
# entrypoint and wrappers: the log is dev-owned 0600, the wrappers write
# only to the drop-box spool, and no chmod loosens it.
- name: Entrypoint token-access audit log not agent-writable (#6287)
run: bash deploy/test_entrypoint_token_access_audit.sh

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/v2-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ env:
# Self-hosted runners sit inside OpenShift; keep these hermetic unit tests from auto-discovering that cluster.
KUBERNETES_SERVICE_HOST: ''
KUBERNETES_SERVICE_PORT: ''
# A slow cache-segment download must never cost more than a recompile.
# Some cluster nodes crawl when pulling from the Actions cache backend —
# observed 16-minute "Restore warm Go build cache" steps against a ~2-minute
# cold compile. Abort a segment after 1 minute and let the job build instead.
ACTIONS_CACHE_SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'


# Cancel superseded PR runs only (#4623). A new push to a PR makes the
Expand Down
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,43 @@ Hive did not historically maintain a complete changelog. This file starts a prag

## Unreleased

## 2026-09-18 (v4.55.1)

### Changed

- `cmd/hive`'s `main.go` shed five self-contained domains into their own files — GitHub App key-file resolution (`appkeyfile.go`), self-upgrade marker/outcome bookkeeping (`selfupgrade.go`), login-required scanning and its sighting debounce (`login_scan.go`), auto-merge eligibility classification (`merge_eligibility.go`), and PR/issue intent verdicts (`intent_verdicts.go`) ([#7238](https://github.com/hivecommons/hive/issues/7238)). `main.go` drops from 9,795 to 8,072 lines. This is pure code motion — every one of the 242 top-level declarations was moved byte-identically, verified by a structural declaration diff, so no operator-visible behaviour changes; it makes the merge, upgrade and App-key paths reviewable on their own instead of only as a slice of a 9.8K-line file.

### Fixed

- Agent dots no longer show red (down) for 10-15+ minutes while the fleet is healthy ([#7526](https://github.com/hivecommons/hive/issues/7526)). `/api/status` — and the replay frame a reconnecting dashboard tab gets on the SSE stream — served a snapshot that was only ever refreshed at the end of an eval cycle, and that cycle enumerates every configured repo against the GitHub API first. On a spoke with sixteen repos under rate-limit backoff the cycle ran far past its nominal `eval_interval_s`, so agents the manager had started (and whose tmux sessions were alive and posting PR reviews) still read `state=stopped` and the whole fleet rendered as crashed. The existing 10-second agent-only tick — in-memory manager state, no GitHub calls — now also patches the agent block of the served snapshot, so liveness on every dashboard surface is at most one tick old regardless of how long the GitHub enumeration takes.
- Tests: the copilot SDK-helper probe tests in `pkg/dashboard` are now hermetic. `TestProbeCopilotModelsSDK_AbsentHelperYieldsSentinel` and `TestProbeCopilotModelsSDK_HelperNotInstalled` used to run whatever was installed at the production helper path, so on any host that ships `copilot-models.mjs` without stored copilot auth (every live agent host) the "absent helper" sentinel never fired and the whole `pkg/dashboard` suite went red with `Not authenticated`. The helper path is now a test-seam var (`setCopilotSDKHelperPathForTest`, mirroring the `knowledge.SetBaseDirForTest` convention) pointed at a temp path, and a new `TestProbeCopilotModelsSDK_FailingHelperIsNotAbsent` covers the previously untestable-on-CI third state — helper present but exiting nonzero (the #7365 case) — via a stub script instead of the real helper.

## 2026-09-18 (v4.55.0)

### Added

- Added `review.max_perspectives_per_pr`, an opt-in cap on how many review perspectives one PR may be given per dispatch cycle. Without it the parallel review budget is spent in PR order, so the head of a deep queue absorbs every slot and adding reviewers buys more opinions on one PR instead of coverage across many. Capping spends the same budget breadth-first, and loses no coverage because skipped perspectives are still dispatched on later cycles. Zero (the default) keeps the existing fan-out behavior.
- Review comments now route: a `requires_human` or `reject` verdict opens with a pinned `**HUMAN DECISION NEEDED**` marker maintainers can filter on, mentions the PR author when that author is a person (never an app/bot account, which notifies nobody), and asks the reviewer to state plainly what it could not judge instead of approving around it.

### Changed

- Split six more domains out of `pkg/agent/manager.go` (consent, copilot auth, env, modes, routing, thrash) into per-domain files matching the v5 layout; manager.go shrinks from 4,321 to 1,793 lines. No behavior change — verbatim block moves.
- Hovering a blocked PR pill on the dashboard now names the branch-protection rule that is holding the PR, instead of GitHub's one-word `blocked` state ([#7515](https://github.com/hivecommons/hive/issues/7515)). GitHub folds every unsatisfied rule — a red required check, a required check that never ran, a missing approval, a reviewer who asked for changes — into that single word, so an operator had to open the PR on GitHub to learn which one, the trip the pill exists to save. The sweep now collects GitHub's own review decision (one GraphQL query per repository per cycle, not one per PR) and compares the base branch's required-check set against the check runs it already walks, and the tooltip reads `blocked — changes requested by @reviewer`, `blocked — required check "validate" has not reported`, or `blocked — required check "build" is failing`. When the facts on hand genuinely do not identify a rule the tooltip still says so plainly rather than guessing, and on a repository that reports its required contexts as commit statuses no "has not reported" claim is made at all.

### Fixed

- Hub: the `auth-url` / `auth-response-headers` annotations [#7457](https://github.com/hivecommons/hive/pull/7457) added to the `hive-contribute` Ingress now reach hosted spokes that were provisioned before that fix ([#7517](https://github.com/hivecommons/hive/issues/7517)). The provisioning template is applied only when a hive is created, so on every pre-existing spoke nginx never asked the hub who was calling and `/api/contribute/me` kept answering `401` to the hive's own signed-in owner — the #7453 symptom, on a spoke whose `served_sha` was well past the fix. A new 15-minute hub sweep (`contribute_ingress_reconcile.go`, alongside the NET_ADMIN and per-hive-env reconciles) reads each hosted spoke's live `hive-contribute` Ingress on nginx clusters and merge-patches the two annotations on when they are missing or stale; a converged Ingress is a no-op, the patch rolls no pod, and the expected values are pinned equal to what the template renders so the sweep and provisioning cannot disagree. OpenShift-Route and pull-only clusters are skipped. `src/docs/security-model.md` now records the general rule: a template change to an existing object needs a reconcile path or a re-provision note.

### Security

- The token-access audit log behind `GET /api/token-access` is no longer writable by the agents it audits ([#6287](https://github.com/hivecommons/hive/issues/6287)). The log records every gh CLI command and git credential lookup an agent makes and is gated at owner role for exactly that reason, yet it was fed by the audited parties: `bin/gh-wrapper.sh` and `bin/git-credential-hive.sh` run as the agent UID and appended straight into `/var/run/hive-metrics/token-access.jsonl`, which only works when the file is writable by every agent (dev:node 0664, and every agent's primary group is node). Append is indistinguishable from write at the permission level, so a compromised or prompt-injected agent could truncate the trail of its own token use, rewrite lines to blame a peer, or forge entries outright. The wrappers now drop one JSON event per call into `/var/run/hive-metrics/token-access-events`, a drop-box the agents can create files in but cannot list (0730, sticky, setgid), and the hive process ingests those events into the log, which is now hive-owned 0600 with no group or other bit at all. On ingest the hive replaces each event's self-reported `uid` with the uid that owns the event file, the same trust anchor the PR-request watcher uses, and keeps a disagreeing claim beside it as `claimed_uid`, so an entry forged in a peer's name lands attributed to the forger. Malformed or oversized events are rejected rather than appended. A 0664 log left by an older image is tightened on every boot and on every ingest pass instead of trusted. What remains open is narrower than before: an agent can still delete its own event during the two-second window before ingest, and can still flood the log with real calls; it can no longer alter anything already recorded.

## 2026-09-18 (v4.54.2)

### Fixed

- Dashboard: hovering a PR pill that is not green now says, in plain words, why the sweep will not merge it and what would unblock it ([#7515](https://github.com/hivecommons/hive/issues/7515)). A PR GitHub reported as not mergeable used to hover as the bare GitHub enum — `not mergeable on GitHub (blocked)` — even though the merge-eligible classifier had just been handed the exact gate that made GitHub say blocked and then dropped it. `blocked` now keeps the sweep's reason (`blocked — CI failing: build, lint`, `blocked — awaiting review approval`, `blocked — held: …`, `blocked — intent verification: …`, `blocked — CI pending`), and when every sweep gate passes it says `blocked — all sweep gates pass; a branch-protection rule is unsatisfied` instead of nothing. The other states read as what to do rather than the API name: `has merge conflicts with v4 — needs a rebase`, `behind v4 — needs an update from the base branch`, `draft — mark ready for review to enter the sweep`, `mergeability not yet computed by GitHub — re-checked next tick`. The base branch comes from the PR list payload (`base_ref`, new on the status snapshot); no extra GitHub calls. Naming the exact branch-protection rule when the sweep itself has no reason (a review GitHub requires, a required check that never reported) is the issue's step 2 and still open.

## 2026-09-18 (v4.54.1)

### Changed
Expand Down
31 changes: 31 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,37 @@ operation during the transition, is part of the
[v5 GA readiness bar](https://github.com/hivecommons/hive/blob/v5/src/docs/v5-ga.md)
(live tracker: [#6016](https://github.com/hivecommons/hive/issues/6016)).

## v6 — Future Line (not open)

There is no v6 branch, milestone, or release channel, and none is planned
until v5 reaches GA. "v6" is a **designation, not a line**: a label for
proposals that deliberately target the horizon after the current one, so
they can be written down without pretending they are scheduled.

Policy for v6-designated work:

- **Design-only.** A v6-designated proposal may land as a design document
under [`src/docs/design/`](src/docs/design/) after normal review.
Implementation PRs for v6-designated work are out of scope on every
current branch and should be closed with a pointer to this section.
- **The line opens after v5 GA.** Opening a v6 line is blocked on the
[v5 GA readiness bar](https://github.com/hivecommons/hive/blob/v5/src/docs/v5-ga.md)
closing (live tracker:
[#6016](https://github.com/hivecommons/hive/issues/6016)). When it opens,
it opens the same way v5 did — public RFC issues gate each workstream,
per [GOVERNANCE.md](GOVERNANCE.md) — and designs parked here re-enter
through that gate rather than being grandfathered in.
- **Designation is cheap and non-binding.** Marking a design "v6" records
intent and preserves the work; it is not acceptance, priority, or a
commitment that a v6 line will include it.

v6-designated designs to date:

- **GitHub @-mention triggers** — a human summons an agent by mentioning
the App on an issue or PR, mirroring the existing Linear inbound-mention
path ([#7483](https://github.com/hivecommons/hive/issues/7483),
[design doc](src/docs/design/github-mention-triggers.md)).

## Hosted Hive Hub

The hosted hub at [hive.hivecommons.dev](https://hive.hivecommons.dev) is the
Expand Down
68 changes: 68 additions & 0 deletions src/cmd/hive/appkeyfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"fmt"
"strings"

"github.com/hivecommons/hive/pkg/apphealth"
)

// deliveredKeyPath is where a hub-delivered private key for appID is stored.
//
// The filename NAMES the App, so a key can only ever be found under the App it
// was delivered for. The generic /data/gh-app-key.pem carries no such evidence:
// a key written there for one App silently becomes "the key" for whatever
// app_id the config later claims, which is how all 33 heartbeat-only-cluster spokes ended up
// signing as the public App with the GHE key and getting
// 404 Integration not found.
//
// Falls back to the generic path only when the delivery names no App, so a key
// is never dropped on the floor.
func deliveredKeyPath(appID int64) string {
if p := appKeys.PerAppIDKeyPath(appID); p != "" {
return p
}
return appKeys.DataKeyPath
}

// describeAppKeyFailure turns a bare wrapped os error from github.NewAppAuth
// into a message an operator can act on without reading the source: it names
// the path actually tried, the full resolution order that produced it, and the
// underlying cause.
//
// The generic "reading app key /secrets/gh-app-key.pem: no such file" that this
// replaces gave no hint that key_file, $GH_APP_KEY_FILE, the PVC path and the
// provisioning mount are all consulted in a fixed order — so the usual response
// was to put the key in the wrong one of the four.
func describeAppKeyFailure(configured, envOverride, resolved string, err error) string {
order := []string{
fmt.Sprintf("$GH_APP_KEY_FILE=%s", describeKeySource(envOverride)),
fmt.Sprintf("github.key_file=%s", describeKeySource(configured)),
fmt.Sprintf("per-app-id PVC key %s/gh-app-key-<app_id>.pem", appKeys.DataDir),
fmt.Sprintf("per-app-id provisioning key %s/gh-app-key-<app_id>.pem", appKeys.ProvisionedDir),
fmt.Sprintf("PVC fallback %s", appKeys.DataKeyPath),
fmt.Sprintf("provisioning mount %s", appKeys.ProvisionedKeyPath),
}
return fmt.Sprintf(
"GitHub App private key could not be loaded from %q: %v. "+
"Resolution order (first non-empty wins): %s. "+
"Write a PEM-encoded RSA private key to that path, or point github.key_file at one.",
resolved, err, strings.Join(order, " → "),
)
}

// describeKeySource renders an unset key-file source as "(unset)" so the
// resolution order in describeAppKeyFailure reads unambiguously.
func describeKeySource(v string) string {
if strings.TrimSpace(v) == "" {
return "(unset)"
}
return v
}

// appKeyPaths snapshots the two App key path locations for a pkg/apphealth
// call. Read at call time on purpose: tests repoint these, and capturing them
// once would silently ignore that.
func appKeyPaths() apphealth.KeyPaths {
return apphealth.KeyPaths{Spoke: appKeys.DataKeyPath, Provisioned: appKeys.ProvisionedKeyPath}
}
Loading
Loading