Commit Graph

1701 Commits

Author SHA1 Message Date
hsiegeln
1809574fe6 ci: include cameleer-license-api in maven deploy project list
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 3m31s
CI / docker (push) Successful in 2m44s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 48s
The license-api module was added in 858975f0 but the CI deploy step's
`-pl` list still only built parent + server-core + minter. server-core
now depends on cameleer-license-api, which wasn't in the registry yet,
so the deploy job failed with:

    Could not find artifact com.cameleer:cameleer-license-api:jar:1.0-SNAPSHOT
    in gitea (https://gitea.siegeln.net/api/packages/cameleer/maven)

Add cameleer-license-api to the project list so it builds and publishes
before its consumers in the same reactor invocation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:41:26 +02:00
hsiegeln
858975f03f refactor(license): extract cameleer-license-api module from server-core
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 2m57s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Splits the pure license contract types (LicenseInfo, LicenseValidator,
LicenseState, LicenseStateMachine, LicenseLimits, DefaultTierLimits) into a
new cameleer-license-api module under package com.cameleer.license.

Why: cameleer-license-minter previously depended on cameleer-server-core for
these types, dragging cameleer-server-core + cameleer-common onto the
classpath of every minter consumer (notably cameleer-saas). The SaaS
management plane has no business carrying server-runtime types — it only
needs the license contract to mint and verify tokens.

After:
  cameleer-license-minter -> cameleer-license-api  (no server internals)
  cameleer-server-core    -> cameleer-license-api
  cameleer-saas           -> cameleer-license-minter -> cameleer-license-api

Verified: mvn -pl cameleer-license-minter dependency:tree shows the minter
no longer pulls cameleer-server-core or cameleer-common. Full reactor
verify (-DskipITs) green: 371 tests pass.

LicenseGate stays in server-core (server-runtime state holder, not contract).

Closes cameleer/cameleer-server#156

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:06:52 +02:00
hsiegeln
30db609aff Merge feature/auth-harmonization: capability-driven login UX
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 3m6s
CI / docker (push) Successful in 2m49s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 53s
Replaces the prompt=none → /login?local trap with a deterministic
capability endpoint (GET /api/v1/auth/capabilities). LoginPage renders
SSO-primary or local form based on caps; ?local is the explicit
admin-recovery escape hatch. Drops prompt=none from the SSO authorize
URL per RFC 9700 §4.4. Adds Vitest + IT coverage and docs.

MFA enrollment / enforcement deferred to issue #154.
2026-04-26 19:52:31 +02:00
hsiegeln
45b5f473c9 refactor(auth): post-review tidy — drop @NotNull, refresh e2e comment, use oidc.primary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:48:20 +02:00
hsiegeln
71688dea16 docs(auth): document AuthCapabilitiesController + login routing 2026-04-26 19:41:20 +02:00
hsiegeln
b63b9aa4bb fix(ui): drop OidcCallback ?local trap on login_required 2026-04-26 19:38:15 +02:00
hsiegeln
7565cdcf2f fix(ui): try/finally in handleOidcLogin; logout redirects to /login (not ?local)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:36:23 +02:00
hsiegeln
b7d390adf4 feat(ui): capability-driven LoginPage; drop prompt=none silent SSO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:29:59 +02:00
hsiegeln
29769480be feat(ui): useAuthCapabilities hook 2026-04-26 19:23:39 +02:00
hsiegeln
657281461d chore(api): regenerate OpenAPI types for /auth/capabilities
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:22:14 +02:00
hsiegeln
af53eca7f6 test(auth): tighten AuthCapabilitiesControllerIT — drop redundant stub, add coverage gaps 2026-04-26 19:17:05 +02:00
hsiegeln
4f6e7ea4dc feat(auth): AuthCapabilitiesController — GET /api/v1/auth/capabilities
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:10:17 +02:00
96fc55b932 Merge pull request 'feature/auth-harmonization' (#155) from feature/auth-harmonization into main
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 3m58s
CI / docker (push) Successful in 37s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
Reviewed-on: #155
2026-04-26 19:01:00 +02:00
hsiegeln
cd92036f91 ci(minter): deploy license-minter JARs to Gitea Maven registry on push
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 4m57s
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 4m35s
CI / docker (pull_request) Has been skipped
CI / docker (push) Successful in 3m44s
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Failing after 2m29s
Adds <distributionManagement> at the parent POM and a push-only deploy
step in the build job. Selects the parent + core + minter via -pl so
both the plain library JAR and the Spring Boot fat CLI JAR are pushed
with their full dep tree resolvable; server-app is excluded as a
fat-jar runtime, not a library.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:59:38 +02:00
hsiegeln
2f7c6aa005 fix(auth): @NotNull on AuthCapabilitiesResponse.Oidc.providerName 2026-04-26 18:59:20 +02:00
hsiegeln
f945d10d48 feat(auth): AuthCapabilitiesResponse DTO 2026-04-26 18:57:09 +02:00
hsiegeln
ddb18c4f17 feat(auth): OidcProviderNameDeriver — issuer URI → display label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:53:31 +02:00
hsiegeln
f1aa1ea19f docs(auth): implementation plan for login routing harmonization
9 tasks, TDD throughout. Backend: OidcProviderNameDeriver utility,
AuthCapabilitiesResponse DTO, AuthCapabilitiesController. Frontend:
useAuthCapabilities hook, capability-driven LoginPage rewrite,
OidcCallback ?local trap removal. Plus docs and manual smoke for
the original SaaS-provisioned tenant bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:46:55 +02:00
hsiegeln
a3c0e9aa7f docs(auth): harmonization design — login routing capability model
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m37s
CI / docker (push) Successful in 2m32s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 53s
Captures the decision to gate login UX on capabilities (no SaaS-mode
flag), drop prompt=none from the primary OIDC flow per RFC 9700 §4.4,
and keep ?local as the explicit admin-recovery escape hatch.

MFA enrollment / enforcement and password reset for local accounts are
explicitly deferred and tracked in issue #154.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:37:00 +02:00
hsiegeln
5216dab043 Merge feature/runtime-hardening: license enforcement (36 tasks)
Delivers a signed-token license tier system with 8 enforced cap
surfaces (envs/apps/agents/users/outbound/alert-rules/compute/jar-
retention), per-tenant validation, daily revalidation, ClickHouse
TTL recompute on license change, audit trail, and Prometheus
metrics. Plus runtime container-hardening pre-work and a stand-
alone cameleer-license-minter Maven module (test-scope only on
the server).

40 license commits + 2 prior runtime-hardening commits +
3 design/spec/plan + 3 docs (minter README, operator guide,
SaaS handoff).

Range: ec51aef8..5864553f
Tasks: 1-36 of docs/superpowers/plans/2026-04-25-license-enforcement.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:33:44 +02:00
hsiegeln
5864553fed docs(license): minter README + operator guide + SaaS handoff
cameleer-license-minter/README.md — vendor-side guide: build, public
LicenseMinter API, CLI usage with all flags, token format (standard
base64, not url-safe), LicenseInfo schema, Ed25519 key generation,
worked example, security guidance, runtime-separation verification.

docs/license-enforcement.md — operator guide: install paths and
priority (env > file > DB > none), public-key config, REST API,
state machine (ABSENT/ACTIVE/GRACE/EXPIRED/INVALID), default tier
caps, 403 envelope semantics, retention TTL recompute, daily
revalidation, audit + Prometheus surfaces, troubleshooting.

docs/handoff/2026-04-26-license-saas-handoff.md — SaaS playbook:
trust model, onboarding/renewal/revocation runbooks, key management,
cap matrix per plan tier, telemetry, failure modes, testing guidance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:33:12 +02:00
hsiegeln
140ea88460 docs(rules): document license enforcement classes + endpoints
Final consolidation pass after the 36-task license-enforcement work.

core-classes.md:
- New license/ section: LicenseInfo, LicenseLimits, DefaultTierLimits,
  LicenseValidator, LicenseGate, LicenseStateMachine, LicenseState.
- runtime/: added CreateGuard (functional interface for license-cap
  hooks consulted by EnvironmentService/AppService/AgentRegistryService).
- admin/: AuditCategory.LICENSE added to the documented enum value list.

app-classes.md:
- New license/ section: LicenseService, LicenseRepository, LicenseRecord,
  PostgresLicenseRepository, LicenseChangedEvent, LicenseEnforcer,
  LicenseUsageReader, LicenseCapExceededException, LicenseExceptionAdvice,
  LicenseMessageRenderer, RetentionPolicyApplier, LicenseRevalidationJob,
  LicenseMetrics.
- LicenseAdminController entry expanded to document the GET response
  shape and the LicenseService.install delegation pattern.
- config/: RuntimeBeanConfig note about CreateGuard wiring; new
  LicenseBeanConfig entry covering the four-bean topology and the
  always-failing-validator fallback.

Note: LicenseChangedEvent, LicenseRepository, LicenseRecord, and
PostgresLicenseRepository live in cameleer-server-app, not -core; the
plan's section assignments were corrected against the actual code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:17:31 +02:00
hsiegeln
581dc1ad13 test(license): SchemaBootstrapIT — assert V5 license + retention columns
Two new assertions: license table has tenant_id/license_id/token/
installed_at/installed_by/expires_at/last_validated_at columns with
expected types + NOT NULL constraints, PK on tenant_id; environments
has execution_retention_days/log_retention_days/metric_retention_days
all integer NOT NULL DEFAULT 1.

Note: V5 migration does not include an installed_via column; the
plan's spec was aspirational. Test asserts what the migration
actually creates (and what PostgresLicenseRepository reads/writes).

OpenAPI regen (Step 35.2) deferred to session end — requires running
backend + UI dev server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:13:50 +02:00
hsiegeln
e198c13e8a test(license): RetentionRuntimeRecomputeIT — TTL recompute on license change
Install license with max_log_retention_days=30, env.configured=60 →
effective=30; verify ClickHouse logs table reflects toIntervalDay(30).
Replace with max=7 → effective=7; verify TTL recomputed. Polls
system.tables.create_table_query up to 5s for the @Async listener
to apply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:08:28 +02:00
hsiegeln
1e78439ddd test(license): LicenseEnforcementIT — cross-cap smoke regression net
Five @Nested cap surfaces (envs, apps, outbound, alert rules, users)
share a single synthetic license with cap=1 each. Each test pushes
just past the cap and verifies the standard 403 envelope plus a
cap_exceeded audit row. Per-limit ITs cover full per-cap behavior;
this IT catches accidental wire-rip regressions across all caps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 16:00:50 +02:00
hsiegeln
1a307da6b2 test(license): LicenseLifecycleIT — install/persist/revalidate/reject
End-to-end IT covering the full lifecycle: mint a token via
cameleer-license-minter (test-scope), POST it via /api/v1/admin/license,
verify state=ACTIVE, clear gate, revalidate from PG, verify state restored.
Plus: tampered signature -> 400 + LICENSE/FAILURE audit row, gate not
mutated to ACTIVE.

Adds cameleer-license-minter as a test-scope dep on cameleer-server-app
(verified absent from runtime/compile classpaths). Also disables the
default spring-boot:repackage execution on the minter pom so the main
artifact stays as a plain library JAR consumable as a Maven dependency
(the cli classifier still produces the executable jar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:56:01 +02:00
hsiegeln
885f2be16b feat(license): Prometheus gauges for state + days remaining
cameleer_license_state{state=...} (one-hot per LicenseState),
cameleer_license_days_remaining (negative when ABSENT/INVALID),
cameleer_license_last_validated_age_seconds. Refreshed on
LicenseChangedEvent and every 60s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:43:54 +02:00
hsiegeln
945ecd78cf 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>
2026-04-26 15:42:39 +02:00
hsiegeln
3f69e546e4 refactor(license): LicenseAdminController delegates to LicenseService
GET returns {state, invalidReason, envelope, lastValidatedAt}. POST
delegates to licenseService.install(token, userId, "api") so install
goes through audit + persistence + event publish. Removes the inline
LicenseValidator construction from the controller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:34:07 +02:00
hsiegeln
340d954fed feat(license): LicenseRevalidationJob — daily cron + 60s post-startup
@Scheduled(cron = "0 0 3 * * *") triggers svc.revalidate() daily.
@EventListener(ApplicationReadyEvent.class) @Async fires once 60s
after boot to catch ABSENT->ACTIVE transitions if the license was
written to PG between server starts. Exceptions are logged but never
propagate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:32:33 +02:00
hsiegeln
484a55f4f4 feat(license): RetentionPolicyApplier listens on LicenseChangedEvent
@EventListener fires on every license install/replace/expire. For each
environment, computes effective TTL = min(licenseCap, env.configured)
and emits one ALTER TABLE ... MODIFY TTL ... per (table, env). Tables
covered: executions, processor_executions, logs, agent_metrics,
agent_events. ClickHouse failures are logged but do not propagate
(listener is async-tolerant).

route_diagrams is intentionally excluded -- it has no TTL clause in
init.sql (ReplacingMergeTree keyed on content_hash, not time-series).
server_metrics is also excluded -- it has no environment column
(server straddles environments).

Per-environment TTL via WHERE requires ClickHouse 22.3+; the project's
current image (clickhouse/clickhouse-server:24.12) is well above that
floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:28:42 +02:00
hsiegeln
cc5d88d708 feat(license): surface execution/log/metric retention days on Environment
Adds three int fields to the Environment record + repository row mapper,
matching the columns added in V5. Default value is 1 per the V5 NOT NULL
DEFAULT 1. Read DTO surfaces the fields via Jackson record serialization;
setter endpoint deferred to a follow-up that wires the corresponding
license cap checks.

The canonical constructor enforces >= 1 for each retention field — V5
guarantees this at the DB level, but the runtime guard catches in-memory
construction errors (e.g., test sites that pass 0).

Test sites updated to the 12-arg signature with retention defaults of 1.
EnvironmentAdminControllerIT gains a regression test asserting the wire
shape exposes all three fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:22:40 +02:00
hsiegeln
046f08fe87 feat(license): enforce max_jar_retention_count at PUT jar-retention
Returns 422 UNPROCESSABLE_ENTITY when jarRetentionCount exceeds
license cap. Default tier cap = 3. The other three retention caps
(execution/log/metric retention days) are deferred to T26+ where
the corresponding fields are added to Environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:16:04 +02:00
hsiegeln
56bddcc747 feat(license): enforce compute caps at DeploymentExecutor PRE_FLIGHT
Adds ComputeUsage record + computeUsage() helper to LicenseUsageReader
that aggregates from PG. DeploymentExecutor.executeAsync runs three
assertWithinCap checks (max_total_cpu_millis, max_total_memory_mb,
max_total_replicas) right after config resolution. The existing
executor try/catch turns a LicenseCapExceededException into a FAILED
deployment with the cap message in the failure reason.

Adds ComputeCapEnforcementIT (HTTP-driven; @MockBean RuntimeOrchestrator,
since cap rejection short-circuits before any orchestrator call) plus
defensive license lifts in BlueGreenStrategyIT, RollingStrategyIT,
DeploymentSnapshotIT, and DeploymentControllerAuditIT so sequential
deploys under testcontainer reuse don't trip the new caps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 15:09:39 +02:00
hsiegeln
71f3b70b86 feat(license): enforce max_alert_rules at AlertRuleController.create
Adds AlertRuleRepository.count() and a LicenseEnforcer.assertWithinCap
call at the top of the POST handler. Default cap = 2; the 3rd rule
gets the standard 403 envelope. Sibling alert ITs that legitimately
need more than 2 rules get the cap lifted via the test-license helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:50:59 +02:00
hsiegeln
5a579415a1 feat(license): enforce max_outbound_connections at OutboundConnectionServiceImpl.create
Adds LicenseEnforcer.assertWithinCap call at the top of create() using
repo.listByTenant(tenantId).size() as the current count. Lifts the cap
in OutboundConnectionAdminControllerIT (duplicateNameReturns409 needs
2 creates in one test). LicenseExceptionAdvice maps the rejection to
the standard 403 envelope; cap_exceeded audit row emitted via the
LicenseEnforcer 3-arg ctor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:40:12 +02:00
hsiegeln
1ff30905f7 feat(license): enforce max_users at user creation paths
Wires LicenseEnforcer into UserAdminController.createUser and
OidcAuthController auto-signup. Cap fires before any validation so
over-cap creates short-circuit cheaply. Audit emission already
present (LicenseEnforcer 3-arg ctor from T16 emits cap_exceeded
under AuditCategory.LICENSE).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:29:54 +02:00
hsiegeln
afdaee628b feat(license): enforce max_agents at AgentRegistryService.register
Adds a CreateGuard to AgentRegistryService that fires only on NEW
registrations: re-registers of an existing agent bypass the cap (they
don't grow the registry, and rejecting them would orphan an agent that
already counts against the cap). Live-only count for cap enforcement —
STALE/DEAD/SHUTDOWN agents are excluded so the cap reflects the working
fleet, not historical residue.

Reuses the CreateGuard pattern from T18-T19. The global
LicenseExceptionAdvice maps the resulting LicenseCapExceededException to
403 with the structured envelope — no AgentRegistrationController
changes needed.

AgentCapEnforcementIT exercises the HTTP path end-to-end: two registers
succeed at cap=2, a third returns 403 with the expected envelope, and a
re-register of an already-registered agent succeeds at-cap.

Sibling agent-registering ITs (Agent*ControllerIT, Diagram*IT,
Execution*IT, Search*IT, Protocol*IT, Backpressure*IT, JwtRefresh*IT,
Registration*IT, Security*IT, SseSigning*IT, IngestionSchemaIT) lift
max_agents in @BeforeEach and clear the synthetic license in @AfterEach
— the in-memory registry is shared across @SpringBootTest reuse
boundaries, so without the lift the default-tier max_agents=5 would be
exhausted by accumulated test residue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:19:08 +02:00
hsiegeln
80dafe685b feat(license): enforce max_apps at AppService.createApp
Adds CreateGuard hook to AppService.createApp using the same pattern
as T18 (EnvironmentService). AppRepository.count() added; the bean
wires LicenseEnforcer.assertWithinCap("max_apps", current, 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:36:34 +02:00
hsiegeln
198811b752 refactor(license-test): rename installTestLicenseWithCaps -> installSyntheticUnsignedLicense
Makes the signature-bypass loud at every call site since T19-T25 will
copy this pattern 5+ more times. The helper still loads via
LicenseGate.load() directly (no signature check) — the new name
ensures any future caller has to acknowledge that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:24:58 +02:00
hsiegeln
8a64a9e04c feat(license): enforce max_environments at EnvironmentService.create
Adds CreateGuard functional interface to core (preserves the no-Spring
boundary between core and app) and wires LicenseEnforcer into the
EnvironmentService bean in RuntimeBeanConfig so POST
/api/v1/admin/environments rejects with the structured 403 envelope
(error/limit/cap/state/message) once the cap is reached. Default tier
max_environments=1; the V1 baseline seeds the default env, so the very
next create through the API is rejected unless a license lifts the cap.

Also adds EnvironmentRepository.count() (with PostgresEnvironmentRepository
impl), TestSecurityHelper.installTestLicenseWithCaps(...) so existing ITs
that POST envs keep working, and a defensive cleanup in
LicenseUsageReaderIT/EnvironmentAdminControllerIT to stay
order-independent under Testcontainer reuse (deletes deployments+apps
before envs to avoid FK violations).

Test: EnvironmentCapEnforcementIT (new) drives the rejection path
end-to-end and asserts the 403 body shape produced by
LicenseExceptionAdvice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:16:41 +02:00
hsiegeln
f291d7c24d feat(license): LicenseUsageReader aggregates current usage
One COUNT per entity table; one SUM-grouped query over non-stopped
deployments for compute caps. SQL traverses
deployed_config_snapshot->'containerConfig' (corrected from the
plan's top-level path; the snapshot record nests containerConfig
under that key). agentCount is fed in by the controller since it's
an in-memory registry value, not a DB row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:47:59 +02:00
hsiegeln
9b9b56043c fix(license): explicit @Autowired ctor + tolerate audit failures
Two follow-ups to LicenseEnforcer review:
- Add @Autowired to the 3-arg ctor so Spring picks it unambiguously
  (the 2-arg test ctor is otherwise an equally-greedy candidate).
- Wrap audit.log() in try/catch + log.warn so a degraded audit DB
  cannot mask a cap rejection: callers still see HTTP 403 even when
  audit storage is unhealthy.
- Extract counter name to private static final.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:43:27 +02:00
hsiegeln
4985348827 feat(license): LicenseEnforcer single entry point
assertWithinCap consults LicenseGate.getEffectiveLimits, throws
LicenseCapExceededException on overflow, increments
cameleer_license_cap_rejections_total{limit=...} for telemetry, and
emits an AuditCategory.LICENSE cap_exceeded audit row when an
AuditService is wired (3-arg ctor; the test-only 2-arg ctor passes
null and the audit call short-circuits). Unknown limit keys are
programmer errors (IllegalArgumentException), not 403s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:36:58 +02:00
hsiegeln
e98d790874 fix: always show user badge for logout access
When username is empty (e.g. email-registered OIDC users with no display
name), the badge was hidden entirely, making logout inaccessible. Always
render the badge with fallback text "Account".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 12:31:25 +02:00
hsiegeln
2bad9c3e48 feat(license): cap-exceeded exception + state-aware message renderer
LicenseCapExceededException + @ControllerAdvice mapping to 403 with a
body that includes state, limit, current, cap, and a per-state human
message templated by LicenseMessageRenderer (covers ABSENT/ACTIVE/
GRACE/EXPIRED/INVALID with day counts and reason). Adds the forState()
overload now (used by the /usage endpoint in Task 30) so both surfaces
share identical phrasing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:26:39 +02:00
hsiegeln
6f658b6648 docs(license): session handoff at task 14/36
Resume point for the next session executing the License Enforcement
plan. Captures: 14 done commit SHAs, what works/doesn't end-to-end,
critical plan deviations (AuditService.log API; LicenseInfo.label
not tier; throwaway-keypair fallback validator; ClickHouse TTL WHERE
caveat for T27), batching strategy, and suggested next-task order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:07:35 +02:00
hsiegeln
b95e80a24a feat(license): wire LicenseService into boot order (env > file > DB)
LicenseBootLoader @PostConstruct calls LicenseService.loadInitial,
which delegates to install() so env-var/file/DB paths share a single
audit + event-publish code path. A missing public key now produces
an always-failing validator (constructed with a throwaway keypair so
the parent ctor accepts it) so loaded tokens route to INVALID
instead of being silently ignored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:16:49 +02:00
hsiegeln
6fbcf10ee4 feat(license): LicenseService + LicenseChangedEvent
Single mediation point for token install/replace/revalidate. Audits
under AuditCategory.LICENSE, persists to PG, mutates the LicenseGate,
and publishes LicenseChangedEvent so downstream listeners
(RetentionPolicyApplier, LicenseMetrics) react uniformly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:11:48 +02:00
hsiegeln
2f75b2865b feat(license): add AuditCategory.LICENSE
Tasks downstream (LicenseService, LicenseEnforcer) audit under
this category for install_license / replace_license / reject_license
/ revalidate_license / cap_exceeded actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:06:07 +02:00