feat(license): LicenseUsageController GET /api/v1/admin/license/usage

Returns state, expiresAt/daysRemaining, lastValidatedAt, message
(LicenseMessageRenderer.forState), and a limits[] array where each
entry carries key/current/cap/source ("license" vs "default"). Adds
public AgentRegistryService.liveCount() so max_agents can be reported
from the in-memory registry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 15:42:39 +02:00
parent 3f69e546e4
commit 945ecd78cf
4 changed files with 234 additions and 6 deletions

View File

@@ -79,20 +79,20 @@ public class AgentRegistryService {
// NEW registration — consult the cap. ConcurrentHashMap.compute propagates the
// exception thrown here, so the controller advice (LicenseExceptionAdvice) maps
// LicenseCapExceededException to 403 with the structured envelope.
registerGuard.check(liveAgentCount());
registerGuard.check(liveCount());
log.info("Agent {} registered (name={}, application={}, env={})", id, name, application, environmentId);
return newAgent;
});
}
/**
* Live-only agent count for license-cap enforcement.
* STALE/DEAD/SHUTDOWN agents are excluded so the cap reflects the working fleet, not
* historical residue. Re-registers of an existing agent revive it via the
* Live-only agent count for license-cap enforcement and the {@code /admin/license/usage}
* surface. STALE/DEAD/SHUTDOWN agents are excluded so the cap reflects the working fleet,
* not historical residue. Re-registers of an existing agent revive it via the
* {@code existing != null} branch in {@link #register}, so this count never double-counts.
*/
private long liveAgentCount() {
return agents.values().stream().filter(a -> a.state() == AgentState.LIVE).count();
public int liveCount() {
return (int) agents.values().stream().filter(a -> a.state() == AgentState.LIVE).count();
}
/**