Skip to content

✨ feat(settings): the GitHub API hourly caps per agent can be set from the dashboard and the admin MCP - #17385

Merged
hivecommons-hive[bot] merged 3 commits into
hivecommons:v6from
Danathar:feat/v6-agent-github-cap-controls
Nov 9, 2026
Merged

hivecommons-hive[bot] merged 3 commits into
hivecommons:v6from
Danathar:feat/v6-agent-github-cap-controls

Conversation

@Danathar

@Danathar Danathar commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Summary

The API proxy's per-agent GitHub API hourly cap can now be changed without editing hive.yaml. Settings → Forge App gets a fleet default control next to the agent reserve floor, each agent's General tab gets its own value, and the admin MCP gets two matching write operations. A save applies to the proxy at once (no restart), is saved to hive.yaml and the dashboard overlay, and is audited with the old and new value. The GitHub API budget panel now lists each agent's cap and its calls in the current hour.

Why

On a hub-managed spoke the hub owns hive.yaml, so the spoke owner had no way to change agents.github_api_hourly_cap or agents.<name>.github_api_hourly_cap. The projectbluefin hive wants the fleet at 300 and the reviewer at 600: the reviewer hits its 300/h cap overnight, and the reviews it writes after that come back as false "requires human".

What changes

  • Fleet default (Settings → Forge App). "Agent GitHub API hourly cap" sits beside the agent reserve floor: a number sets agents.github_api_hourly_cap, 0 removes the cap, a blank field removes the key so the default 300 applies. It saves through PUT /api/config/github as agent_github_api_hourly_cap (null = back to the default), the same path and audit (config_github) as the reserve floor.
  • One agent (General tab). "GitHub API hourly cap": a number, 0 for no cap, blank to inherit. It saves through PUT /api/config/agent/{name}/general as githubApiHourlyCap (null = inherit), audited as config_agent_general. Replicas and review heads are copies of their base agent, so their tab shows the inherited value read-only and the endpoint refuses an override on them (400 naming the base agent). The save rebuilds the copies at once, so they follow the base straight away.
  • Live apply. main.go hands the proxy b.cfg.AgentGitHubAPIHourlyCap bound to the live *Config, and the dashboard's Config is that same pointer, so both saves change what the proxy enforces on its next request. Tests pin this through the bound method value.
  • Admin MCP. agent.github_api_hourly_cap (agent, cap: number or inherit) and github.agent_api_hourly_cap (cap: number or default), through write_preview / write_confirm. The preview reads the current value: raising a cap, removing one, or inheriting a higher fleet value is disclosed as widening, naming the App installation's shared bucket and github.agent_reserve_floor; lowering is not. If the current value cannot be read, the preview says the change may be widening.
  • Visibility. The existing "GitHub API budget" panel (no new card) gains an agent caps table: cap, where it comes from, calls counted in the current hour, share used. The counts come from a new read-only proxy accessor in its own file (agent_github_budget_view.go). config_effective_read shows both keys' values (added to the non-secret value list) and a new agent_github_api_hourly_caps block with each agent's effective cap.
  • Settings parity. The two TODO #11854 allowlist lines for these keys are removed; both controls carry a YAML hint and a settingHelpMark.
  • Docs. Runtime and quota (new "Changing the agent caps" subsection: controls, MCP ops, inheritance order, replicas, live apply, the reserve floor), admin MCP, API reference, and a changelog fragment.

Not changed: the 300 default, the reserve floor value, the App's bucket, and the cap getter itself (Refs #17379 covers the getter's fallback). Kept clear of #17376 and #17378, which edit agent_github_budget.go.

Files

  • Dashboard: api_config.go, api_agent_config.go, forge_apps.go, status_builder.go, api_diag_config.go, new api_github_api_cap.go, github_api_cap_view.go, diag_github_api_caps.go; static-src/js/{config-tabs,config-agent,debug-logs,shimmy-handlers}.js and the regenerated static/index.html; settings_parity_allowlist.txt, diag_config_value_keys.txt.
  • Config: new pkg/config/github_api_cap_settings.go (fleet getter and setter).
  • Proxy: new crux/proxy/agent_github_budget_view.go; cmd/hive/main.go wires it to the dashboard.
  • Admin MCP: new pkg/adminmcp/tools_github_cap_ops.go, registered in tools_agent_ops.go.

Tests

None run locally (repo AGENTS.md); CI is the verdict. New tests:

  • TestFleetAgentGitHubCapSavesAuditsAndAppliesLive: bad values are refused; 600, then 0, then null each change the cap the proxy-bound getter returns, the overlay, the forge-apps answer and the audit detail.
  • TestAgentGitHubCapGeneralSaveInheritZeroAndReplicas: the override reaches the agent and its replica at once, lands in the overlay and the agent file, 0 is kept, null clears it, a replica is refused, and an unrelated save leaves the cap alone.
  • TestGHRateLimitsCarryAgentCaps, TestDiagConfigShowsAgentGitHubCaps, TestGitHubCapControlsRender, TestAgentGitHubCallsLastHourMatchesTheCapWindow.
  • Admin MCP: widening disclosed for raise / 0 / higher inherit and not for lower, same, lower inherit or an uncapped agent; replicas and bad values refused; confirm executes the dashboard endpoints.

On the current v6 tip each of these fails: the two endpoints ignore the new fields (so the getter keeps returning 300), the response fields, the agent caps table, the proxy accessor and the controls do not exist, and the two write ops are not registered.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1

The API proxy's per-agent GitHub API hourly cap (agents.github_api_hourly_cap
and agents.<name>.github_api_hourly_cap) could only be changed in hive.yaml,
which on a hub-managed spoke only the hub can write. A reviewer that ran out
of its 300 calls overnight had its later reviews come back as false
"requires human" with no way for the spoke owner to raise its cap.

- Settings -> Forge App: a fleet default control beside the agent reserve
  floor (number; 0 = no cap; blank = the default 300), saved through
  PUT /api/config/github as agent_github_api_hourly_cap (null removes the key).
- Each agent's General tab: its own cap (number; 0 = no cap; blank = inherit),
  saved through PUT /api/config/agent/{name}/general as githubApiHourlyCap.
  A replica or review head shows its base agent's cap read-only and the
  endpoint refuses an override on it.
- Both write the live config the proxy reads (main.go binds the cap getter to
  the same *Config), so a save applies on the next proxied request with no
  restart; both save to hive.yaml and the dashboard overlay and audit the old
  and new value.
- Admin MCP: agent.github_api_hourly_cap and github.agent_api_hourly_cap
  write ops. The preview reads the current value and discloses a raise, a
  removal, or inheriting a higher fleet value as widening, naming the App
  installation's shared bucket and github.agent_reserve_floor.
- The GitHub API budget panel lists each agent's cap, its source and its calls
  in the current hour (ghRateLimits.agent_caps, from a new read-only proxy
  accessor); config_effective_read shows both keys' values and each agent's
  effective cap.
- The two settings-parity allowlist entries are removed, and the operator,
  admin MCP and API reference docs describe the controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1
Signed-off-by: Danathar <Danathar@users.noreply.github.com>
@kubestellar-prow kubestellar-prow Bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Nov 9, 2026
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign danathar for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Open PR overlap check

Base branch: v6

Conflicts are checked with every PR merged onto the current tip of v6, so a PR that only needs a rebase is not reported as conflicting with its siblings.

No conflicting open PRs or same-file overlaps were found.

@hivecommons-hive

Copy link
Copy Markdown
Contributor

@Danathar this PR is back in your court (stage/072-returned-to-contributor). The hive will not merge it until the head changes.

Review asks:

  • the branch conflicts with the base and needs a rebase

Reply here or push a new head to this branch (same sign-off); CI and the review swarm re-run automatically and the PR returns to the normal path.

Queued in your relay fix queue (1 waiting): fixes are handed out before new work as soon as you have capacity.

…p-controls

Signed-off-by: Douglas Baggett <doug.baggett@gmail.com>

# Conflicts:
#	src/docs/operator-reference/runtime-and-quota.md

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness review (head f55e50d)

1. Fleet cap set from the dashboard does not survive a Kubernetes reload/restart — the hub-managed case the PR is for.

PUT /api/config/github → applyFleetAgentGitHubCap (src/pkg/dashboard/api_github_api_cap.go:44-48) sets cfg.AgentsGitHubAPIHourlyCap{,Set} on the live config, and Config.Save() does marshal it into /data/hive.yaml.dashboard (src/pkg/config/github_api_budget.go, marshalConfigYAMLWithAgentCap). But the overlay reader adopts only a fixed set of fields: loadWithDashboardOverlay (src/pkg/config/load.go:149-248) copies OTel/Tracing, Governor.WorkSource, operator cadences, RemovedAgents and Agents from the overlay and nothing else. overlay.AgentsGitHubAPIHourlyCap is never read, so on a pod restart or a ConfigMap-remount reload (the fsnotify watcher goes through LoadWithDashboardOverlay, see save.go:117-125) the fleet cap reverts to the hub's seed. In Kubernetes "the ConfigMap seed plus the overlay is authoritative" (save.go:164), so the hive.yaml write does not rescue it either.

The per-agent override is fine — overlay.Agents entries replace the base wholesale (agent_overlay.go:204-228), so agents.reviewer.github_api_hourly_cap: 600 survives. Only the new fleet control is affected. The PR body and changelog say the save "is saved to … the dashboard overlay" (true) and that this lets a hub-managed spoke change agents.github_api_hourly_cap without hive.yaml (not durably). TestFleetAgentGitHubCapSavesAuditsAndAppliesLive reads the overlay bytes back (github_api_cap_settings_test.go, overlay assertions around the 600 case) but never reloads through LoadWithDashboardOverlay, so it cannot catch this.

Suggested fix, before the fullness guard in load.go (alongside the WorkSource adoption), plus a reload assertion in the test:

 	if !overlay.Governor.WorkSource.IsZero() {
 		cfg.Governor.WorkSource = overlay.Governor.WorkSource
 	}
+	// agents.github_api_hourly_cap is a dashboard-editable fleet key; the
+	// overlay is the only layer the dashboard can write on a hosted spoke.
+	if overlay.AgentsGitHubAPIHourlyCapSet {
+		cfg.AgentsGitHubAPIHourlyCap = overlay.AgentsGitHubAPIHourlyCap
+		cfg.AgentsGitHubAPIHourlyCapSet = true
+	}

(Adjacent, pre-existing and out of scope here: github.agent_reserve_floor, edited on the same tab, has the same gap — overlay.GitHub is not adopted at load.go:174-248.)

Everything else I checked holds up: the proxy view (agent_github_budget_view.go:22-46) uses the same now/window as check/pruneLocked and takes the same mutex; handleAgentConfigGeneral writes agentCfg back and calls ExpandAgentReplicas (api_agent_config.go:871-877) so replicas pick up the base's cap; AgentGitHubAPIHourlyCap handles a nil *Config, so buildAgentGitHubCaps with cfg == nil is safe.

— hive: agent=adjudicator backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Review follow-ups (below review.backlog.min_severity: P2)

Non-blocking findings below the backlog filing line are listed here instead of as issues, one comment per PR, updated in place. Tick them off as they are fixed; set review.backlog.min_severity: P3 to file every finding.

  • P3 low correctness — github.agent_reserve_floor has the same overlay-adoption gap (pre-existing) (src/pkg/config/load.go:174): loadWithDashboardOverlay (src/pkg/config/load.go:174-248) never adopts overlay.GitHub, so github.agent_reserve_floor saved from the same Forge App tab via PUT /api/config/github also reverts to the C…

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the diff against the v6 tree, including the callers the new code leans on. This looks correct to me; nothing blocking found.

What I verified:

  • Replicas follow the base at runtime. ExpandAgentReplicas copies the whole AgentConfig (pkg/config/config.go:373), so a replica carries its own GitHubAPIHourlyCap and AgentGitHubAPIHourlyCap() (pkg/config/github_api_budget.go:160) would read the stale copy first — but handleAgentConfigGeneral already re-runs ExpandAgentReplicas() after the field write (pkg/dashboard/api_agent_config.go:847), so the copies are rebuilt on the same save. TestAgentGitHubCapGeneralSaveInheritZeroAndReplicas pins scanner-2 at 600 after the PUT, which is the right assertion for this path.
  • null on the fleet key really removes it. SetFleetAgentGitHubAPIHourlyCap(nil) clears AgentsGitHubAPIHourlyCapSet, and marshalConfigYAMLWithAgentCap only emits agents.github_api_hourly_cap when that flag is set (pkg/config/github_api_budget.go:88), so a save after "back to default" writes no key rather than a literal 300.
  • Proxy read is non-mutating. callsInWindow (crux/proxy/agent_github_budget_view.go) only reads b.events under the lock and skips stale entries without pruning, so the Debug panel cannot change what the next cap check sees. The test cross-checks the count against b.check.
  • Admin MCP wiring. githubCapWriteOps() is appended in DefaultWriteOps; optionalIntArg/requiredString exist in the package; widening logic (githubCapWidens) treats 0 as uncapped on both sides, matching the getter's semantics.
  • Changelog fragment is a single - bullet; diag_config_value_keys.txt additions are non-secret integers.

One non-blocking note: readFleetGitHubCap in the MCP preview reads settings.agent_github_api_hourly_cap, which forgeGitHubSettingsFor fills with FleetAgentGitHubAPIHourlyCap() (300 when unset), so "default → 600" is disclosed as widening from 300 rather than from "default"; that is the correct comparison, just noting the wording is intentional.

CI had not reported when I read this (tide pending, Go jobs queued), so this is a tree read, not a test result.

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

@hivecommons-hive

hivecommons-hive Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Hive status — updated in place; edits do not notify. Full history is in the Activity Monitor.

Review findings from reviewer-2 (folded here because this head already carries its hive review):

Read the diff against v6 plus the callers it touches. Looks correct to me; nothing blocking found. What I checked, for whoever signs off on an XXL change:

  • Live apply is real. main.go:4332 binds b.cfg.AgentGitHubAPIHourlyCap (method value on the live *Config) into the proxy, and both handlers mutate that same config, so pkg/config/github_api_cap_settings.go + api_config.go applyFleetAgentGitHubCap change what check() sees on the next request without a restart.
  • Replica staleness — not an issue. ExpandAgentReplicas does derived := agent (shallow copy, config.go:371), so I expected a replica expanded while its base already carried an override to keep the old *int after a base save (the agent's own value wins first in AgentGitHubAPIHourlyCap, github_api_budget.go:160). But handleAgentConfigGeneral already re-runs ExpandAgentReplicas after writing the base (api_agent_config.go:846-847), so replicas are rebuilt with the new pointer. Note the new test only exercises a replica that started with a nil override, so it would pass even without that re-expansion; it is the existing handler code, not the test, that guarantees the "replicas follow at once" claim.
  • Window parity. agent_github_budget_view.go callsInWindow uses at.After(cutoff); pruneLocked drops !After(cutoff) (agent_github_budget.go:146) — same boundary, so the panel count matches the cap check. Read-only under b.mu, no pruning.
  • Set/unset round-trip. SetFleetAgentGitHubAPIHourlyCap(nil) resets to the default with AgentsGitHubAPIHourlyCapSet=false, which marshalConfigYAMLWithAgentCap (github_api_budget.go:88) reads to omit the key, so blank in the UI removes the key rather than writing 300. json.RawMessage keeps null vs absent distinct; validate() is called before any mutation (api_config.go:316).
  • Admin MCP write ops go through the existing write_preview/write_confirm path and disclose widening; githubCapWidens treats 0 as unbounded in both directions. Replicas are refused at preview and at the endpoint.
  • Docs, changelog fragment (changelog.d/added-agent-github-cap-controls.md) and diag_config_value_keys.txt all updated with the behaviour; parity allowlist TODOs removed rather than added.

CI was still pending when I read it; I have not run anything locally per AGENTS.md.

Evidence bundle `#173859715e04` sha256:8f045cae624d6cdfcee0ada08729155ca9a4399ee14e412b07781a4825ff44d8 — download from the dashboard or `hivectl review evidence #17385`.

@hivecommons-hive
hivecommons-hive Bot merged commit 5bfc094 into hivecommons:v6 Nov 9, 2026
71 of 72 checks passed
@hivecommons-hive

Copy link
Copy Markdown
Contributor

merged by Hive trusted-author auto-merge: author @Danathar holds role owner and GitHub permission maintain on repo; CI green at 9715e04.

hivecommons-hive Bot added a commit that referenced this pull request Nov 9, 2026
…17403)

* 🐛 [v6] contribute: never dispatch rework for a merged or closed PR

#17385 merged at 13:13:58Z; at ~14:25Z the hub still
handed Danathar's relay "fix-pr hive#17385", and the relay ran it twice.

Hub:
- Queue a nudged PR under owner/name, not the nudge's Display (the bare
  configured name on a hive that lists repos without the org). Resolve
  keys on owner/name, so the bare entry was never removed when the PR
  left stage 072, and the relay could not match the task's repo "hive"
  to an owner/name.
- Before dispatching a queued fix, check its PR is still open (state
  cache first, else one PR read tagged hive:contribute_fix_dispatch).
  Merged or closed PRs leave the queue with a log line; an unreadable
  state holds the dispatch for this tick. fixqueue.Next previews what a
  dispatch would take so only those PRs are checked.
- Send a boundary fix task once: the ready handler ships the returned
  message, so sendFixTask no longer pushes it as well.
- Progress for an in-flight fix task (a boundary fix runs as the relay's
  primary task) is not a resume claim and is no longer revoked.

Relay:
- A fix-pr task_id the relay already runs (primary, parallel, preempting
  or deferred) is ignored on a second task_assign, with a log line naming
  where it runs; a fix that becomes the primary task says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1
Signed-off-by: Danathar <Danathar@users.noreply.github.com>

* test(contribute): a retried boundary fix's progress is not revoked (utah-packages#972)

projectbluefin/utah-packages#972, 2026-11-09: the relay refused
fix-karibhou-1, the hub requeued it and served it at the next ready as
fix-karibhou-2 on the same connection, and the relay's progress for the
retry (repo empty, client_gen 1870) was answered with "no matching
server-issued lease" and task_revoke at 14:50:21Z and 14:54:36Z.

The handleTaskProgress change in this PR already covers it; this test
drives the real fail -> requeue -> boundary dispatch path and sends
progress without a repo, as the relay did, so the retried task id must be
tracked on the connection and never revoked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1
Signed-off-by: Danathar <Danathar@users.noreply.github.com>

* contribute: an unreadable PR state no longer keeps the relay idle

When a queued fix's PR state could not be read, selectFixTask answered
task_unavailable/fix_pr_in_flight, so the relay got no new issue either.
That happens when GitHub is struggling or the API budget is low, the time
the hive needs its contributors most. The fix now stays queued and is
retried on the next ready, and the relay takes ordinary work meanwhile.
Busy fix slots still answer task_unavailable as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1
Signed-off-by: Douglas Baggett <doug.baggett@gmail.com>

---------

Signed-off-by: Danathar <Danathar@users.noreply.github.com>
Signed-off-by: Douglas Baggett <doug.baggett@gmail.com>
Co-authored-by: Danathar <Danathar@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: hivecommons-hive[bot] <280983584+hivecommons-hive[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant