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>
This commit is contained in:
171
docs/superpowers/plans/2026-04-26-license-enforcement-handoff.md
Normal file
171
docs/superpowers/plans/2026-04-26-license-enforcement-handoff.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user