Skip to content

feat(open-core): /api/runtimes catalog + locked-but-visible switcher hook#2209

Draft
vivekchand wants to merge 1 commit into
mainfrom
feat/runtime-catalog-endpoint
Draft

feat(open-core): /api/runtimes catalog + locked-but-visible switcher hook#2209
vivekchand wants to merge 1 commit into
mainfrom
feat/runtime-catalog-endpoint

Conversation

@vivekchand
Copy link
Copy Markdown
Owner

Summary

Phase 5 foundation on top of the open-core entitlement plumbing landed in #2186 / #2189: expose every known runtime — including paid ones with zero local sessions — through a stable GET /api/runtimes catalog so the global runtime switcher can render an upgrade affordance against the runtimes this install isn't entitled to observe.

Headline invariant: no behaviour change in grace mode (the default). The catalog reports locked=False for every row, the frontend's locked set stays empty, and the dropdown renders exactly as it did before this endpoint existed. The locked-but-visible affordance only activates once CLAWMETRY_ENFORCE=1 flips the resolver out of grace.

Changes

  • clawmetry/entitlements.py: add RUNTIME_LABELS (single source of truth shared with _CM_RT_LABEL in app.js), runtime_label(), and runtime_catalog() returning [{id, label, free, allowed, locked}] for every known runtime. Stable ordering: free first, paid after, both alphabetical. Never raises — any resolution error falls back to a grace catalog.
  • routes/entitlement.py: register GET /api/runtimes on bp_entitlement, returning {runtimes, grace, enforced}. Never-raise; any error falls back to the OpenClaw-only grace shape so the UI always has something safe to render.
  • clawmetry/static/js/app.js: _cmInitGlobalRuntimeSwitcher() now also fetches /api/runtimes and folds locked runtimes into _cmLockedRuntimes only when enforced=true, so grace-mode installs see no change. When enforcement is on, locked runtimes appear in the dropdown as 🔒 <Label> · Upgrade alongside the observed ones.
  • Tests (19 passing):
    • tests/test_entitlements.py — 4 new tests: grace-locks-nothing, enforced-locks-paid, label coverage for every paid runtime, unknown-runtime label fallback.
    • tests/test_routes_runtimes.py — 3 Flask-test-client tests for the new endpoint: grace shape, enforced shape, key stability (defends against an accidental rename breaking the dropdown).
$ python3 -m pytest tests/test_entitlements.py tests/test_routes_runtimes.py -q
...................                                                      [100%]
19 passed in 0.22s
$ ruff check clawmetry/entitlements.py routes/entitlement.py
All checks passed!

$ node --check clawmetry/static/js/app.js
(syntax ok)

Smoke test against the live endpoint:

# grace (default)
GET /api/runtimes → {"enforced": false, "grace": true, "runtimes": [
  {"id": "openclaw", "label": "OpenClaw", "free": true, "locked": false}, ...
  {"id": "claude_code", "label": "Claude Code", "free": false, "locked": false}, ...
]}

# CLAWMETRY_ENFORCE=1
GET /api/runtimes → {"enforced": true, "grace": false, "runtimes": [
  {"id": "openclaw", "free": true, "locked": false},
  {"id": "claude_code", "free": false, "locked": true}, ...10 paid runtimes locked
]}

Why a draft

The open-core rollout is in grace mode until the announced enforce date — this PR is the next foundational layer (Phase 5 in entitlements.py:327). Keeping it draft so the local operator handles the daemon-side verification + cloud-snapshot decrypt before this lands.

Local operator verification checklist

I cannot reach the live daemon, ~/.openclaw, or the cloud snapshot from this sandbox — please run the steps below before flipping to ready-for-review:

  • Copy changed files into the daemon venv:
    cp clawmetry/entitlements.py routes/entitlement.py clawmetry/static/js/app.js \
        ~/.clawmetry/lib/python*/site-packages/clawmetry/ # adjust for your tree
    
  • Clear __pycache__ under the venv and kickstart the daemon:
    find ~/.clawmetry -name __pycache__ -exec rm -rf {} +
    launchctl kickstart -k gui/$(id -u)/com.clawmetry.sync
    
  • Hit the new endpoint locally:
    curl -s localhost:8900/api/runtimes | jq .
    
    Expect grace=true, enforced=false, every paid runtime present with locked=false.
  • Set CLAWMETRY_ENFORCE=1 and re-hit it: expect locked=true on the 10 paid runtimes, openclaw still free; then unset.
  • Decrypt the live cloud snapshot and browser-check the dashboard: the runtime switcher renders unchanged in grace mode (no padlocks). With CLAWMETRY_ENFORCE=1 set, paid runtimes appear as 🔒 <Label> · Upgrade even when they have zero sessions.
  • CI green on this draft before flipping out of grace.

Out of scope

  • Per-runtime "this runtime is locked behind Pro" empty-state copy on each tab (separate UI follow-up; this PR is the data layer + switcher hook only).
  • License-key flow + clawmetry-pro download (handled by feat: Ed25519 self-hosted license client (clawmetry activate) #2189; this PR doesn't touch license.py).
  • Any pricing / monetization copy — kept entirely out of this repo by policy.

🤖 Generated with Claude Code


Generated by Claude Code

…hook

Phase 5 foundation: surface every known runtime — including paid ones with
zero local sessions — so the global runtime switcher can render an upgrade
affordance against runtimes the install isn't entitled to observe. No
behaviour change in grace mode (the default): the catalog reports
locked=False for every row, the frontend's locked set stays empty, and the
dropdown renders exactly as it did before this endpoint existed.

- clawmetry/entitlements.py: add RUNTIME_LABELS (single source of truth
  shared with app.js's _CM_RT_LABEL), runtime_label(), and
  runtime_catalog() returning [{id,label,free,allowed,locked}] for every
  known runtime. Stable ordering: free first, paid after, both alphabetical.
- routes/entitlement.py: register GET /api/runtimes on bp_entitlement,
  returning {runtimes, grace, enforced}. Never-raise: any resolution error
  falls back to the OpenClaw-only grace shape.
- clawmetry/static/js/app.js: _cmInitGlobalRuntimeSwitcher() now also
  fetches /api/runtimes and folds locked runtimes into _cmLockedRuntimes
  ONLY when enforced=true, so grace-mode installs see no change. When
  enforcement is on, locked runtimes appear in the dropdown as
  "🔒 <Label> · Upgrade" alongside the observed ones.
- tests: 4 new entitlements tests (grace-locks-nothing, enforced-locks-paid,
  label-coverage, label-fallback) + a 3-test Flask client suite for the new
  endpoint covering both grace and enforced shapes plus key stability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Visual diff

Comparing 473216446254 (head) against the PR base branch.

No flagged differences (running with ALWAYS_POST=1).

View Before After Diff
desktop overview before after diff · 0.23%
desktop flow before after diff · 0.00%
desktop brain before after diff · 0.00%
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.03%
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%
mobile overview before after diff · 0.23%
mobile flow before after diff · 0.00%
mobile brain before after diff · 0.03%
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%

Folder: pr/2209/473216446254. 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.

github-actions Bot pushed a commit that referenced this pull request May 28, 2026
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