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

172 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:** `b95e80a2``feat(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 `LicenseService`** — `update(...)` 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:
```java
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. **T15**`LicenseCapExceededException` + `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. **T16**`LicenseEnforcer`. Single new class, depends on `LicenseGate.getEffectiveLimits()`. Add the 3-arg constructor with `AuditService` for `cap_exceeded` audit emission (used by Task 21).
3. **T17**`LicenseUsageReader`. 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
```bash
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
```