# 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 1–14 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
```
