Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -4334,6 +4334,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