Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog.d/added-agent-github-cap-controls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The API proxy's per-agent GitHub API hourly cap can now be changed without editing `hive.yaml`, so an operator on a hub-managed spoke can give one agent more room. Settings → Forge App sets the fleet default (`agents.github_api_hourly_cap`: a number, `0` for no cap, blank for the default of 300) next to the agent reserve floor, and each agent's General tab sets its own value (`agents.<name>.github_api_hourly_cap`: a number, `0` for no cap, blank to inherit). The admin MCP gets the same two writes, `agent.github_api_hourly_cap` and `github.agent_api_hourly_cap`, whose preview discloses a raised or removed cap as widening. A save applies to the proxy at once, is saved to the dashboard overlay and audited; replicas and review heads follow their base agent, and the agent reserve floor still applies. Debug → GitHub API budget now lists each agent's cap and its calls in the current hour, and `config_effective_read` shows both values and each agent's effective cap. See [Changing the agent caps](src/docs/operator-reference/runtime-and-quota.md#changing-the-agent-caps).
2 changes: 2 additions & 0 deletions src/cmd/hive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4337,6 +4337,8 @@ func (b *boot) bootProxyWith(deps bootProxyDeps) {
b.githubProxy.SetAgentGitHubBudgetNudgeFunc(b.agentMgr.SendGitHubAPIBudgetNudge)
}
dashboard.SetProxyViolationsProvider(b.githubProxy.Violations)
// Per-agent calls in the cap window, for the GitHub API budget panel.
dashboard.SetAgentGitHubCallsProvider(b.githubProxy.AgentGitHubCallsLastHour)
// Lets the dashboard narrow the LiteLLM model dropdown to the set the
// configured key is entitled to, learned by the proxy from a key-info
// probe or a "team not allowed" 403.
Expand Down
46 changes: 46 additions & 0 deletions src/crux/proxy/agent_github_budget_view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package proxy

import "time"

// AgentGitHubCallsLastHour reports, per agent, how many GitHub API responses
// the proxy has counted against that agent's hourly cap in the current
// sliding hour: the same window the cap check reads. It is read-only (stale
// entries are skipped, not pruned), so calling it never changes what the next
// cap check sees.
//
// The window only grows while an agent has a cap: the proxy records a call
// only when the agent's cap is above 0, so an uncapped agent reads 0 here
// whatever it has spent, and a cap switched on starts from an empty window.
func (p *GitHubProxy) AgentGitHubCallsLastHour() map[string]int {
out := map[string]int{}
if p == nil || p.githubBudget == nil {
return out
}
return p.githubBudget.callsInWindow()
}

func (b *agentGitHubBudget) callsInWindow() map[string]int {
out := map[string]int{}
if b == nil {
return out
}
now := time.Now()
if b.now != nil {
now = b.now()
}
cutoff := now.Add(-agentGitHubBudgetWindow)
b.mu.Lock()
defer b.mu.Unlock()
for agent, events := range b.events {
n := 0
for _, at := range events {
if at.After(cutoff) {
n++
}
}
if n > 0 {
out[agent] = n
}
}
return out
}
42 changes: 42 additions & 0 deletions src/crux/proxy/agent_github_budget_view_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package proxy

import (
"net/http"
"testing"
"time"
)

// AgentGitHubCallsLastHour reports the same sliding-hour count the cap check
// reads, per agent, and reading it changes nothing the next check sees.
func TestAgentGitHubCallsLastHourMatchesTheCapWindow(t *testing.T) {
now := time.Date(2026, 11, 9, 12, 0, 0, 0, time.UTC)
b := newAgentGitHubBudget(func() time.Time { return now })
p := &GitHubProxy{githubBudget: b}

b.record("reviewer", http.StatusOK, 300)
now = now.Add(30 * time.Minute)
b.record("reviewer", http.StatusOK, 300)
b.record("reviewer", http.StatusNotModified, 300) // a 304 is not charged
b.record("scanner", http.StatusOK, 300)
b.record("quality", http.StatusOK, 0) // uncapped: no window kept

got := p.AgentGitHubCallsLastHour()
if got["reviewer"] != 2 || got["scanner"] != 1 || len(got) != 2 {
t.Fatalf("calls = %v, want reviewer 2, scanner 1, nothing else", got)
}

// 31 minutes on, the first reviewer call has left the window.
now = now.Add(31 * time.Minute)
got = p.AgentGitHubCallsLastHour()
if got["reviewer"] != 1 || got["scanner"] != 1 {
t.Fatalf("calls after the window slid = %v, want reviewer 1, scanner 1", got)
}
if count, _, _ := b.check("reviewer", 300); count != got["reviewer"] {
t.Fatalf("cap check counts %d, view counts %d", count, got["reviewer"])
}

var nilProxy *GitHubProxy
if got := nilProxy.AgentGitHubCallsLastHour(); len(got) != 0 {
t.Fatalf("nil proxy = %v", got)
}
}
52 changes: 39 additions & 13 deletions src/docs/admin-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -554,10 +554,16 @@ The keys and names whose values are shown:
`governor.logging.max_backups`, `governor.logging.compress`,
`governor.logging.kick_log_source`, `governor.logging.read_scan_max_mb`,
`agents.*.backend`, `agents.*.model`, `agents.*.enabled`, `agents.*.paused`,
and the per-repository merge policy: `auto_merge.squash_only[*]`,
the API proxy's GitHub API hourly cap `agents.github_api_hourly_cap` and
`agents.*.github_api_hourly_cap`, and the per-repository merge policy: `auto_merge.squash_only[*]`,
`auto_merge.require_current_base[*]` and `auto_merge.human_merge_paths.*[*]`
(the path globs listed under each `owner/repo`). A `*` segment is one map key
and `[*]` any list index.
- **Per-agent GitHub API caps:** the result also carries
`agent_github_api_hourly_caps`, the API proxy's cap as this configuration
resolves it: `fleet` and `fleet_set` for `agents.github_api_hourly_cap`,
`agents` mapping each declared agent to the cap the proxy applies to it
(`0` = no cap), and `overrides` naming the agents with their own value.
- **Environment names:** `HIVE_CONFIG`, `HIVE_ADMIN_MCP_ENABLE_WRITES`,
`HIVE_ADMIN_MCP_PENDING_FILE`, `HOME`, `HOSTNAME`, `LANG`, `PATH`, `TZ`,
`KUBERNETES_SERVICE_HOST`, `KUBERNETES_SERVICE_PORT`
Expand Down Expand Up @@ -674,18 +680,38 @@ The audit log cannot tell an assistant-mediated write from a dashboard action.

### Registered operations

| Area | Operations |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agents | `agent.pause`, `agent.resume`, `agent.nudge`, `agent.restart`, `agent.add`, `agent.remove`, `agent.model`, `agent.backend`, `agent.effort`, `agent.interaction_tier` |
| Fleet | `fleet.autonomy_level` |
| Plans | `plan.propose`, `plan.approve`, `plan.reject` |
| Governor | `governor.feature_settings`, `governor.thresholds`, `governor.threshold_scaling` |
| Repositories | `repository.pause`, `repository.resume`, `repository.item_hold` |
| Activity monitor row actions | see below |
| Budget | `budget.update`, `budget.reset`, `budget.ignore` |
| Contributors | `contributor.trust`, `contributor.agent_role`, `contributor.agent_role_grants`, `contributor.revoke`, `contributor.requeue`, `contributor.delete` |
| Backup | `backup.create` (the archive itself is not returned through MCP) |
| Circuit breaker | `circuit_breaker.engage`, `circuit_breaker.release` |
| Area | Operations |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Agents | `agent.pause`, `agent.resume`, `agent.nudge`, `agent.restart`, `agent.add`, `agent.remove`, `agent.model`, `agent.backend`, `agent.effort`, `agent.interaction_tier`, `agent.github_api_hourly_cap` |
| GitHub | `github.agent_api_hourly_cap` |
| Fleet | `fleet.autonomy_level` |
| Plans | `plan.propose`, `plan.approve`, `plan.reject` |
| Governor | `governor.feature_settings`, `governor.thresholds`, `governor.threshold_scaling` |
| Repositories | `repository.pause`, `repository.resume`, `repository.item_hold` |
| Activity monitor row actions | see below |
| Budget | `budget.update`, `budget.reset`, `budget.ignore` |
| Contributors | `contributor.trust`, `contributor.agent_role`, `contributor.agent_role_grants`, `contributor.revoke`, `contributor.requeue`, `contributor.delete` |
| Backup | `backup.create` (the archive itself is not returned through MCP) |
| Circuit breaker | `circuit_breaker.engage`, `circuit_breaker.release` |

The two GitHub API cap operations change the API proxy's per-agent hourly
cap, the same settings as the dashboard's Forge App and agent General
controls:

- `agent.github_api_hourly_cap` takes `agent` and `cap`: a whole number, `0`
for no cap, or `inherit` to follow the fleet default. Set it on a base
agent; a replica or review head is refused, because it takes its base
agent's cap.
- `github.agent_api_hourly_cap` takes `cap`: a whole number, `0` for no cap,
or `default` to remove `agents.github_api_hourly_cap` so the default of 300
applies.

The preview reads the current value first. Raising a cap, removing one, or
inheriting a higher fleet value is disclosed as widening, naming the App
installation's shared hourly bucket and the agent reserve floor
(`github.agent_reserve_floor`); lowering is not. When the current value cannot
be read, the preview says the change may be widening. See
[Changing the agent caps](operator-reference/runtime-and-quota.md#changing-the-agent-caps).

The Activity monitor's row actions:

Expand Down
2 changes: 1 addition & 1 deletion src/docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ always resolved server-side from the validated token.
| `GET` | `/api/diag/runs/{key}/detail` | Dashboard auth/session (pane tails need read-write, as in the wrapped endpoint) | Diagnostic read (#11943): wraps `GET /api/runs/{key}/detail`, scrubbed; `raw_log` keeps its newest bytes and events/stages/prs/notes their newest entries to stay under 128 KiB, reported in `trimmed`. Wrapped by `run_detail_read`; see [admin-mcp.md](admin-mcp.md#diagnostic-reads) | `pkg/dashboard/api_diag.go:45` |
| `GET` | `/api/diag/health` | Public (as `/api/health/deep`) | Diagnostic read (#11943): wraps `GET /api/health/deep`, scrubbed and returned whole. Wrapped by `health_deep_read`; see [admin-mcp.md](admin-mcp.md#diagnostic-reads) | `pkg/dashboard/api_diag.go:46` |
| `GET` | `/api/diag/prompt-history` | Read-write role (checked by the wrapped endpoint) | Diagnostic read (#11943): wraps `GET /api/prompt-history` (`?agent=&windowHours=` passed through); every entry scrubbed, each prompt cut at 8 KiB (`cut`, `full_bytes`), newest entries kept within 128 KiB with `truncated` and `entries_total`. Wrapped by `prompt_history_read`; see [admin-mcp.md](admin-mcp.md#diagnostic-reads) | `pkg/dashboard/api_diag.go:47` |
| `GET` | `/api/diag/config` | Owner only, verified by this hive (dashboard token or verified owner session) | Diagnostic read (#11943): the effective configuration as `GET /api/config/provenance` computes it — every key and environment name with the layer that set it (`also_set_by` for overridden layers, rejected overlay and last-good fallback in `provenance`); values only for keys and names on the documented `value_list`, all others set/not set; shown values scrubbed; the file itself never returned; `?offset=` pages keys. Wrapped by `config_effective_read`; see [admin-mcp.md](admin-mcp.md#diagnostic-reads) | `pkg/dashboard/api_diag.go:48` |
| `GET` | `/api/diag/config` | Owner only, verified by this hive (dashboard token or verified owner session) | Diagnostic read (#11943): the effective configuration as `GET /api/config/provenance` computes it — every key and environment name with the layer that set it (`also_set_by` for overridden layers, rejected overlay and last-good fallback in `provenance`); values only for keys and names on the documented `value_list`, all others set/not set; shown values scrubbed; the file itself never returned; `?offset=` pages keys. `agent_github_api_hourly_caps` gives the API proxy's GitHub API hourly cap as the configuration resolves it: `fleet`, `fleet_set`, `agents` (declared agent → effective cap; 0 = no cap) and `overrides` (agents with their own value). Wrapped by `config_effective_read`; see [admin-mcp.md](admin-mcp.md#diagnostic-reads) | `pkg/dashboard/api_diag.go:48` |
| `GET` | `/api/leaderboard/battle-log` | Public | Scrubbed public Battle Log activity feed for leaderboard widgets. | `pkg/dashboard/battle_log.go:108` |
| `GET` | `/api/leaderboard/hive-of-week` | Public | Featured weekly project metadata, configured render URLs (empty when none), gource source URL, and embed snippets. | `pkg/dashboard/battle_log.go:109` |
| `GET` | `/api/leaderboard/gource-log` | Public | Gource custom log source for a project/week, derived from public hive event data. | `pkg/dashboard/battle_log.go:110` |
Expand Down
8 changes: 4 additions & 4 deletions src/docs/api-reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Part of the [Dashboard REST API reference](../api-reference.md); see that page f
| `PUT` | `/api/config/variables/{name}` | Owner only | Variable Upsert | `pkg/dashboard/api.go:61` |
| `DELETE` | `/api/config/variables/{name}` | Owner only | Variable Delete | `pkg/dashboard/api.go:62` |

| `GET` | `/api/config/agent/{name}` | Dashboard auth/session | Agent Config Get | `pkg/dashboard/api.go:207` |
| `PUT` | `/api/config/agent/{name}/general` | Owner only | Agent Config General | `pkg/dashboard/api.go:208` |
| `GET` | `/api/config/agent/{name}` | Dashboard auth/session | Agent Config Get; `general` carries the API proxy's GitHub API hourly cap: `githubApiHourlyCap` (this agent's `agents.<name>.github_api_hourly_cap`, `null` = inherit), `githubApiHourlyCapEffective` (the cap the proxy applies now; 0 = no cap), `githubApiHourlyCapFleet` (the fleet default) and `githubApiHourlyCapBase` (the base agent of a replica or review head, else empty) | `pkg/dashboard/api.go:207` |
| `PUT` | `/api/config/agent/{name}/general` | Owner only | Agent Config General; `githubApiHourlyCap` sets `agents.<name>.github_api_hourly_cap` (a whole number, 0 = no cap, `null` = inherit the fleet default), applies to the proxy without a restart and to the agent's replicas and review heads; refused (400) on a replica or review head. Audited as `config_agent_general` with the old and new cap | `pkg/dashboard/api.go:208` |
| `PUT` | `/api/config/agent/{name}/cadences` | Owner only | Agent Config Cadences | `pkg/dashboard/api.go:209` |
| `PUT` | `/api/config/agent/{name}/models` | Owner only | Agent Config Models | `pkg/dashboard/api.go:210` |
| `PUT` | `/api/config/agent/{name}/pipeline` | Owner only | Agent Config Pipeline | `pkg/dashboard/api.go:211` |
Expand Down Expand Up @@ -73,8 +73,8 @@ Part of the [Dashboard REST API reference](../api-reference.md); see that page f
| `DELETE` | `/api/config/governor/agents/{name}` | Owner only | Governor Remove Agent | `pkg/dashboard/api.go:326` |
| `PUT` | `/api/config/governor/repos` | Owner only | Governor Repos | `pkg/dashboard/api.go:327` |
| `POST` | `/api/config/governor/repos/check-access` | Dashboard auth/session | Governor Repo Check Access | `pkg/dashboard/api.go:331` |
| `PUT` | `/api/config/github` | Owner only | Config GitHub | `pkg/dashboard/api.go:332` |
| `GET` | `/api/config/github/forge-apps` | Dashboard auth/session | Config GitHub Forge Apps | `pkg/dashboard/api.go:335` |
| `PUT` | `/api/config/github` | Owner only | Config GitHub; `agent_github_api_hourly_cap` sets the fleet default `agents.github_api_hourly_cap` (a whole number, 0 = no cap, `null` = remove the key so the default 300 applies), applies to the proxy without a restart, audited as `config_github` with the old and new cap; the answer echoes `agent_github_api_hourly_cap` and `agent_github_api_hourly_cap_set` | `pkg/dashboard/api.go:332` |
| `GET` | `/api/config/github/forge-apps` | Dashboard auth/session | Config GitHub Forge Apps; `settings.agent_github_api_hourly_cap` (the fleet default as applied) and `settings.agent_github_api_hourly_cap_set` (whether the key is written), with `settings.defaults.agent_github_api_hourly_cap` (300) | `pkg/dashboard/api.go:335` |
| `GET` | `/api/config/sidebar` | Dashboard auth/session | Sidebar Get | `pkg/dashboard/api.go:394` |
| `PUT` | `/api/config/sidebar` | Dashboard auth/session | Sidebar Set | `pkg/dashboard/api.go:395` |
| `GET` | `/api/config/backends` | Dashboard auth/session | Backends | `pkg/dashboard/api.go:396` |
Expand Down
Loading
Loading