Skip to content

feat(entitlements): min_tier_for_all + extend /required-tier-batch to capacity axes#3230

Merged
vivekchand merged 1 commit into
mainfrom
feat/entitlement-min-tier-for-all
Jun 21, 2026
Merged

feat(entitlements): min_tier_for_all + extend /required-tier-batch to capacity axes#3230
vivekchand merged 1 commit into
mainfrom
feat/entitlement-min-tier-for-all

Conversation

@vivekchand

Copy link
Copy Markdown
Owner

Summary

Closes a gap in /api/entitlement/required-tier-batch: it only accepted features=<csv> and runtimes=<csv>, even though the singular /api/entitlement/required-tier supports all five axes (feature, runtime, channels, retention_days, nodes). A dashboard mixing axes ("you're using fleet + claude_code + 5 channels + 30-day retention + 2 nodes — what tier covers this?") needed N round-trips and a max-by-rank on the client.

Changes

  • clawmetry/entitlements.py — new module-level helper min_tier_for_all(*, features, runtimes, channels, retention_days, nodes). Folds the five per-axis min_tier_for_* helpers + max-by-rank into one canonical reverse lookup. retention_days=None means axis not supplied (NOT unlimited → Enterprise, which would mis-route every call that omits retention). Never raises.
  • routes/entitlement.py/api/entitlement/required-tier-batch now also accepts channels=<int>, retention_days=<int>, nodes=<int>. Blank / non-int values are silently treated as "not supplied" (matches the singular endpoint's never-crash posture rather than mis-routing a typo to Enterprise). The 400 "supply at least one axis" guard now covers all five axes. Existing features= / runtimes= contract unchanged.
  • tests/test_entitlements_min_tier_batch.py — 17 new tests pinning min_tier_for_all semantics (no-args → None, single-axis matches singular helper, most-constraining axis wins, capacity None ≠ unlimited, unknown items skipped) + the extended endpoint contract (capacity-only calls satisfy "at least one axis", blank/non-int capacity is silently skipped, mixed five-axis calls resolve in one round-trip).

Posture

  • Grace by default. No Entitlement defaults change; resolved entitlement is still grace=True, all allows_* checks still return True. This PR only adds introspection surface; no enforcement gate flips. Safe to ship.
  • Backward-compatible response shape. The existing keys (features, runtimes, required_tier, required_tier_rank, current_tier, current_tier_rank, upgrade_required, allowed) keep their meaning; three new keys (channels, retention_days, nodes) are added alongside.

Test plan

  • python -m py_compile clean on changed files
  • pytest tests/test_entitlements_min_tier_batch.py — 41 passed (24 pre-existing + 17 new)
  • pytest tests/test_entitlement*.py tests/test_entitlements*.py — 641 passed
  • CI green on this PR
  • Local operator verification (see below)

Local operator verification checklist

This runs in a remote sandbox with no access to the user's machine, the live daemon, ~/.openclaw, or the cloud snapshot. Verifying live requires the local box:

  1. Pull the branch locally: git fetch origin feat/entitlement-min-tier-for-all && git checkout feat/entitlement-min-tier-for-all.
  2. Copy changed files into the installed package:
    cp clawmetry/entitlements.py ~/.clawmetry/lib/python*/site-packages/clawmetry/
    cp routes/entitlement.py ~/.clawmetry/lib/python*/site-packages/routes/
    
  3. Clear __pycache__ under both clawmetry/ and routes/.
  4. Restart the daemon: launchctl kickstart -k gui/$(id -u)/com.clawmetry.sync.
  5. Hit the extended endpoint:
    curl -s 'http://localhost:8900/api/entitlement/required-tier-batch?features=fleet&runtimes=claude_code&channels=5&retention_days=30&nodes=2' | jq
    
    Expect required_tier: "cloud_starter" (most-constraining axis), allowed: true (grace mode), and the new channels / retention_days / nodes keys echoed back as ints.
  6. Confirm the existing two-axis call still works unchanged:
    curl -s 'http://localhost:8900/api/entitlement/required-tier-batch?features=sso&runtimes=claude_code' | jq
    
    Expect required_tier: "enterprise" (no behaviour change vs main).
  7. Decrypt the live cloud snapshot in the browser and confirm the dashboard renders normally (no regression in grace-mode behaviour).

🤖 Generated with Claude Code


Generated by Claude Code

…r-batch to capacity axes

The /api/entitlement/required-tier-batch endpoint took features=<csv> and
runtimes=<csv> only -- a dashboard mixing axes ("you're using fleet +
claude_code + 5 channels + 30-day retention + 2 nodes -- what tier?")
needed N round-trips and a max-by-rank on the client.

* clawmetry/entitlements.py: new module-level min_tier_for_all(*, features,
  runtimes, channels, retention_days, nodes) that folds the five
  per-axis min_tier_for_* helpers + max-by-rank into one canonical
  reverse lookup. retention_days=None means "axis not supplied" (NOT
  unlimited -> Enterprise, which would mis-route every call that omits
  retention); the per-axis posture matches the plural helpers' "unknown
  contributes nothing, never raise" contract.

* routes/entitlement.py: /api/entitlement/required-tier-batch now also
  accepts channels=<int>, retention_days=<int>, nodes=<int>. Blank /
  non-int values are treated as "not supplied" (matches the singular
  endpoint's never-crash posture). The 400 "supply at least one axis"
  guard now covers all five axes.

* tests/test_entitlements_min_tier_batch.py: 17 new tests pinning
  min_tier_for_all semantics + the extended endpoint contract (capacity-
  only calls satisfy the "at least one axis" rule, blank/non-int
  capacity is silently skipped, mixed-axis calls take the most-
  constraining tier across all five).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Visual diff

Comparing 005eaa52d6d6 (head) against the PR base branch.

No flagged differences (running with ALWAYS_POST=1).

View Before After Diff
desktop overview before after diff · 0.22%
desktop flow before after diff · 0.00%
desktop brain before after diff · 0.01%
desktop usage before after diff · 0.00%
desktop crons before after diff · 0.00%
desktop memory before after diff · 0.00%
desktop security before after diff · 0.02%
desktop subagents before after diff · 0.00%
desktop transcripts before after diff · 0.00%
desktop logs before after diff · 0.00%
desktop skills before after diff · 0.00%
desktop models before after diff · 0.00%
desktop approvals before after diff · 0.00%
desktop alerts before after diff · 0.00%
desktop notifications before after diff · 0.00%
desktop context before after diff · 0.00%
desktop limits before after diff · 0.00%
desktop clusters before after diff · 0.00%
desktop history before after diff · 0.00%
desktop channels before after diff · 0.00%
desktop dives before after diff · 0.00%
desktop harness before after diff · 0.00%
desktop inventory before after diff · 0.00%
desktop nemoclaw before after diff · 0.00%
desktop policy before after diff · 0.00%
desktop selfevolve before after diff · 0.00%
desktop swimlane before after diff · 0.00%
desktop tool-catalog before after diff · 0.00%
desktop tracing before after diff · 0.00%
desktop turn-anatomy before after diff · 0.04%
desktop version-impact before after diff · 0.00%
desktop context-economics before after diff · 0.00%
mobile overview before after diff · 0.13%
mobile flow before after diff · 0.01%
mobile brain before after diff · 0.00%
mobile usage before after diff · 0.00%
mobile crons before after diff · 0.00%
mobile memory before after diff · 0.00%
mobile security before after diff · 0.00%
mobile subagents before after diff · 0.00%
mobile transcripts before after diff · 0.00%
mobile logs before after diff · 0.00%
mobile skills before after diff · 0.00%
mobile models before after diff · 0.00%
mobile approvals before after diff · 0.00%
mobile alerts before after diff · 0.00%
mobile notifications before after diff · 0.00%
mobile context before after diff · 0.00%
mobile limits before after diff · 0.00%
mobile clusters before after diff · 0.00%
mobile history before after diff · 0.00%
mobile channels before after diff · 0.00%
mobile dives before after diff · 0.00%
mobile harness before after diff · 0.00%
mobile inventory before after diff · 0.00%
mobile nemoclaw before after diff · 0.00%
mobile policy before after diff · 0.00%
mobile selfevolve before after diff · 0.00%
mobile swimlane before after diff · 0.00%
mobile tool-catalog before after diff · 0.00%
mobile tracing before after diff · 0.00%
mobile turn-anatomy before after diff · 0.02%
mobile version-impact before after diff · 0.00%
mobile context-economics before after diff · 0.00%

Folder: pr/3230/005eaa52d6d6. Full PNGs also attached as a workflow artefact.

Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem.

@vivekchand vivekchand marked this pull request as ready for review June 21, 2026 03:04
@vivekchand vivekchand merged commit 38b45ba into main Jun 21, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants