Files
cameleer-server/docs/superpowers/plans/2026-04-26-license-enforcement-handoff.md
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

11 KiB
Raw Blame History

License Enforcement — Session Handoff (2026-04-26)

Pick up at Task 15 in docs/superpowers/plans/2026-04-25-license-enforcement.md.

Where we are

  • Branch: feature/runtime-hardening (the three doc commits + 14 implementation commits stack on top of unrelated runtime-hardening work — extract to a clean branch later).
  • Last implementation commit: b95e80a2feat(license): wire LicenseService into boot order (env > file > DB).
  • Tasks 114 of 36 are complete and committed. All tests green.
  • Plan file: docs/superpowers/plans/2026-04-25-license-enforcement.md — 36 tasks, ~4083 lines.
  • Spec file: docs/superpowers/specs/2026-04-25-license-enforcement-design.md.

Resume command for the next session

Resume the License Enforcement implementation at Task 15.
Plan: docs/superpowers/plans/2026-04-25-license-enforcement.md
Handoff: docs/superpowers/plans/2026-04-26-license-enforcement-handoff.md
Last commit: b95e80a2. 14 of 36 tasks done. Branch: feature/runtime-hardening.
Use Subagent-Driven Development. Continue batching where it makes sense
(see "Batching strategy" below).

Done, with commit SHAs

# SHA Subject
1 551a7f12 refactor(license): remove dead Feature enum and isEnabled scaffolding
2 2ebe4989 feat(license): expand LicenseInfo with licenseId, tenantId, grace period
3 cf84d80d feat(license): require licenseId + tenantId in validator
4 ddc0b686 feat(license): add LicenseLimits, DefaultTierLimits, LicenseStateMachine
5 0499a54e feat(license): rewrite LicenseGate around state + effective limits
6 896b7e6e feat(license-minter): add cameleer-license-minter Maven module
7 1ae5a1a2 feat(license-minter): implement LicenseMinter library
8 7300424a feat(license-minter): add LicenseMinterCli (without --verify)
9 f6657f81 feat(license-minter): --verify round-trips before shipping
10 20aefd5b feat(license): Flyway V5 — license table + environments retention columns
11 2e51deb5 feat(license): PostgresLicenseRepository + LicenseRecord
12 2f75b286 feat(license): add AuditCategory.LICENSE
13 6fbcf10e feat(license): LicenseService + LicenseChangedEvent
14 b95e80a2 feat(license): wire LicenseService into boot order (env > file > DB)

Plus three doc commits before T1 (already on branch): 0e512a3c (spec), e0be6a06 (spec revision), ec51aef8 (plan).

What works end-to-end now

  • A signed license token can be installed via env var, file, admin REST POST, or auto-loaded from PG on boot.
  • Validator rejects missing/blank licenseId/tenantId, tenant mismatch, missing exp, expired-past-grace, and signature failure.
  • LicenseService mediates install/replace/revalidate, audits under AuditCategory.LICENSE, persists to PG, mutates LicenseGate, publishes LicenseChangedEvent.
  • LicenseGate.getState() returns ABSENT|ACTIVE|GRACE|EXPIRED|INVALID; getEffectiveLimits() merges over defaults in ACTIVE/GRACE, defaults-only otherwise.
  • Standalone cameleer-license-minter module produces tokens via LicenseMinter.mint(info, privateKey) or the LicenseMinterCli (with --verify round-trip). Confirmed NOT in the runtime dependency tree.
  • PG schema: license table + 3 retention columns on environments migrated cleanly via Flyway V5.

What does NOT yet work

  • No enforcement. Nothing checks LicenseGate.getEffectiveLimits() yet. The default-tier caps exist as constants but no service calls assertWithinCap.
  • No usage reporting. /api/v1/admin/license/usage does not exist.
  • No retention recompute. LicenseChangedEvent is published but no listener acts on it.
  • No daily revalidation. LicenseService.revalidate() exists but no scheduler calls it.
  • No metrics. No Prometheus gauges yet.
  • LicenseAdminController still bypasses LicenseServiceupdate(...) constructs its own LicenseValidator inline. Task 29 fixes this.

Critical deviations from the plan to remember

1. AuditService API (impacts Tasks 15, 21, 28, 29)

The plan assumed AuditService.record(category, action, actor, payload). The actual API is a concrete class with two log(...) overloads:

void log(String action, AuditCategory category, String target,
         Map<String,Object> detail, AuditResult result, HttpServletRequest request)

void log(String username, String action, AuditCategory category, String target,
         Map<String,Object> detail, AuditResult result, HttpServletRequest request)

LicenseService (Task 13) uses the explicit-username variant with request = null. Use the same pattern in any new AuditService call sites in Tasks 15+.

2. LicenseInfo.label JSON ingest (impacts any new fixture)

The validator parses label from the JSON label field, not tier. Old fixtures that only set "tier":"X" produce info.label() == null. When writing new tests that need a non-null label, add "label":"X" to the JSON.

3. Plan said "no change needed" for LicenseValidatorTest in Task 2 — wrong

info.tier() accessor doesn't exist; it's info.label(). T2 implementer fixed this. If any later task asks you to construct LicenseInfo and assert on a "tier", use label.

4. LicenseValidator always-failing fallback (Task 14)

When CAMELEER_SERVER_LICENSE_PUBLICKEY is unset, LicenseBeanConfig.licenseValidator() constructs an anonymous subclass whose validate() always throws. The parent ctor is fed a freshly-generated throwaway Ed25519 public key so it accepts the input. Don't try to use a static all-zero base64 string — Ed25519 rejects the all-zero point at construction time.

5. ClickHouse ALTER TABLE … MODIFY TTL … WHERE — Task 27 caveat

The plan's RetentionPolicyApplier SQL (ALTER TABLE … MODIFY TTL … WHERE environment = '…') requires ClickHouse 22.x+ for per-row TTL with WHERE. Verify against cameleer-server-app/src/main/resources/clickhouse/init.sql before implementing. If unsupported, fall back to global TTL = min(licenseCap, max(env.configured)).

6. Per-test compile dependency between modules

After modifying cameleer-server-core, the next mvn -pl cameleer-server-app test will fail compilation if you don't first run mvn -pl cameleer-server-core install -DskipTests. The implementer subagents already know this — make sure new ones do too.

7. The previous code-review noted (still deferrable)

  • LicenseValidator.label is read via root.get("label").asText() which returns the literal string "null" for a JSON null value rather than Java null. Current fixtures don't trip this; defer.
  • LicenseValidator.validate(...) is ~70 lines doing format-split + signature-verify + payload-parse. Splitting into helpers would help maintainability but no behavioral change required.

Batching strategy that worked

The plan specifies one task = one commit. For mechanical plan-following tasks (T1, T7, T8, T9, T10, T12, T28, T31, T36) one implementer dispatch handles one or more tasks fine. Successful batches in this session:

  • T4 + T5 — both touch cameleer-server-core/src/main/java/com/cameleer/server/core/license/, T5 needs T4's types.
  • T6 + T7 + T8 + T9 — all in the new cameleer-license-minter module, sequential.
  • T10 + T11 + T12 — persistence layer (migration + repo + audit enum).

Tasks NOT to batch (require judgment + per-task review):

  • T18-T25 wiring tasks — each touches existing service code with potential downstream test fallout. Dispatch one at a time.
  • T26 (Environment record fields) — touches every new Environment(...) call site; high blast radius.
  • T27 (RetentionPolicyApplier) — ClickHouse SQL judgment + Async event handling.
  • T32, T33, T34 — integration tests, each large and independent. One at a time, allow full mvn verify runs.

Pre-existing working-tree dirt — leave alone

These predate this work:

M AGENTS.md
M CLAUDE.md
?? docs/superpowers/plans/2026-04-24-cmdk-attribute-filter.md
?? execution-api-response.json
?? overlay-screenshot.png

Subagents were instructed not to stage them. Continue that policy.

Suggested next-session task order

  1. T15LicenseCapExceededException + LicenseMessageRenderer + LicenseExceptionAdvice. Pure new code, mechanical. Single commit. Use LicenseMessageRenderer.forCap(...) AND forState(...) (the latter is needed for the /usage endpoint in T30 — add it now even though the plan only mentions forCap).
  2. T16LicenseEnforcer. Single new class, depends on LicenseGate.getEffectiveLimits(). Add the 3-arg constructor with AuditService for cap_exceeded audit emission (used by Task 21).
  3. T17LicenseUsageReader. Single new class. Verify the deployments.deployed_config_snapshot JSONB shape against DeploymentConfigSnapshot before committing — the plan SQL assumes specific field names.
  4. T18-T20 — wire envs / apps / agents (one at a time, full IT each).
  5. T21 — wire users + cap_exceeded audit emission (this is when the enforcer's audit ctor matters).
  6. T22-T23 — wire outbound + alert rules (one at a time).
  7. T24 — wire compute caps (DeploymentExecutor PRE_FLIGHT). Largest single wiring task — the IT will need a real Docker flow stub.
  8. T25 — wire retention + jar caps.
  9. T26 — Environment record retention fields. High blast radius. Use gitnexus_context first.
  10. T27 — RetentionPolicyApplier (handle ClickHouse caveat).
  11. T28 — LicenseRevalidationJob.
  12. T29-T31 — REST surface + metrics.
  13. T32-T34 — integration tests.
  14. T35 — SchemaBootstrapIT extension + OpenAPI regen.
  15. T36.claude/rules/*.md updates.

Key files reference

  • Plan: docs/superpowers/plans/2026-04-25-license-enforcement.md
  • Spec: docs/superpowers/specs/2026-04-25-license-enforcement-design.md
  • Domain: cameleer-server-core/src/main/java/com/cameleer/server/core/license/
  • App license layer: cameleer-server-app/src/main/java/com/cameleer/server/app/license/
  • Boot config: cameleer-server-app/src/main/java/com/cameleer/server/app/config/LicenseBeanConfig.java
  • PG migration: cameleer-server-app/src/main/resources/db/migration/V5__license_table_and_environment_retention.sql
  • Minter: cameleer-license-minter/
  • AuditService: cameleer-server-core/src/main/java/com/cameleer/server/core/admin/AuditService.java

Verification commands at handoff

git log --oneline b95e80a2 -1                              # confirm last commit
git log --oneline ec51aef8..b95e80a2 --no-merges           # 14 commits in range
mvn -pl cameleer-server-core install -DskipTests           # publish core to local repo
mvn -pl cameleer-server-core test                          # 122 tests, all pass
mvn -pl cameleer-license-minter test                       # 7 tests, all pass
mvn -pl cameleer-server-app test -DskipITs                 # 230 tests, all pass
mvn -pl cameleer-server-app verify -Dit.test=PostgresLicenseRepositoryIT,SchemaBootstrapIT
mvn dependency:tree -pl cameleer-server-app | grep license-minter   # MUST be empty