HELM-16 monetization foundation (spec) - #53
Merged
Conversation
Consolidates the approved entitlements-architecture + org/teams design records into a buildable plan (now that accounts shipped): Free + per-seat Rigel Pro (audits/cloud-connect/agent gated), in-app Stripe Checkout/Portal, 3 slices with gate-enforcement last. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three subagent-driven-development plans derived from the monetization foundation spec: Slice A (schema + entitlements resolver + Stripe setup), Slice B (checkout/portal + in-app billing window + account panel), Slice C (entitlement provider + flip the gates + upgrade prompts). Gate-enforcement last. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix the cancel/grace contradiction (grace = unreachable resolver only, not a real cancellation) in spec + Slice C. - Make EntitlementPayload's boundary mirrors decisive + type-precise (server copy had audits: string[]). - Slice C: 'replace' the Slice B entitlements IPC (avoid double ipcMain.handle); concretize the audit-env + autonomy-gate locate steps; wire personalOrgId from useAccount; add the missing agent-autonomy control gating + upgrade prompt; manual-refresh action. - Slice B: correct the org-shape test mock (kind), add seat count, endpoint invariant note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review found /entitlements used stricter case-sensitive bearer parsing than /me's regex, so a request could auth on one and 401 on the other. Extract the shared sha + bearer into authToken.ts; both routes now parse identically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o the browser Review follow-ups on Slice B: - /billing/checkout|portal now 400 on a missing/non-string orgId (was an unhandled 500 on malformed body); guard runs before any db call. - The in-app Stripe BrowserWindow gets a setWindowOpenHandler that sends any popup (3DS/wallet) to the system browser instead of an untracked child — parity with the main window. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Desktop SIGNUP_ENDPOINT is overridable via RIGEL_SIGNUP_ENDPOINT (release builds stay on api.rigel.run) so a test build can hit a test signups backend. - signups BILLING_ENDPOINT is env-overridable to match. - Boot-time Stripe mode log (from the key prefix) + fail-fast guard: refuses to start on a key/price MODE mismatch (tolerates a network blip verifying it, so Stripe availability never gates the auth backend). New stripeKeyMode + adapter.priceLivemode helpers, tested. - db-secret.example.yaml + docs/stripe-setup.md document the two-Secret model (rigel-signups=live, rigel-signups-test=test), the never-mix rule, the Customer Portal activation, and record the sandbox ids created via the CLI/MCP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Account plan section: iconized Free/Pro card with subline + Upgrade to Pro - Cloud-connect gate: ProGateCard (Unlock cloud clusters) - Assistant autonomy gate: ProGateCard (Unlock the in-cluster agent) - Audit skills: inline ProLockRow (PRO pill + Upgrade) - First-run onboarding: dismissible UpgradeBanner on the free plan Gating logic, entitlement/upgrade wiring, and 402 handling unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Downgrade = non-destructive soft revert (agent→advisory, keep contexts + notifs); crux is the in-cluster agent has no entitlement awareness. Two-layer enforcement: server writes assistant-config on downgrade (desktop open) + agent self-checks /entitlements (backstop). Draft for Fable review. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fable review found a live gap: installing the agent seeded mode:auto with no Pro check, and the agent fail-OPENED to auto on a missing/garbage config key — so a Free user (or a lapsed one reinstalling) could get a fully autonomous agent, bypassing the setMode gate entirely. - Install now seeds mode: advisory (packages/k8s). auto is only ever reachable through setMode, which is Pro-gated — so a fresh install never grants autonomous remediation without an entitled opt-in. - The agent fail-closes: a missing or garbage mode resolves to advisory, never auto (agent/runtimeConfig). A broken/absent config can no longer act on its own. - Test fixtures that asserted execution now set mode:auto explicitly; added lock-in tests for the fail-closed default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d in) Layer 1 also writes autofixEnabled:false + edge-triggers on a genuine free (not the no-cache default); Layer 2 is a signed Ed25519 lease (org-scoped) the agent verifies with a baked-in public key — no credential in-cluster, works air-gapped. Slices L1/L2/L3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Free users on the ready phase now see a single AssistantGate teaser (recorded incident count + masked rows + Upgrade to Pro) instead of the live agent tabs, replacing the scattered per-control ProGateCards. The header's status pill and Pause/Resume controls are hidden until entitled. Loading/install/setup phases render unchanged so a free user can still install the agent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the 'while you were away' incident-count teaser with a full-width feature showcase (hero + 6 capability cards + Upgrade CTA). Drops the d.clusterState.audit dependency entirely, so the gate never surfaces an incident number that could read as 'your cluster is broken'. Pure upsell of what Pro unlocks: autonomous remediation, diagnosis, notifications, autofix PRs, digests, audits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Explicit refetches right after a Stripe upgrade must not read a
60s-stale "free" from makeResolver's in-memory cache. Add an
optional {fresh:true} opt that skips the cache read and always
re-resolves live from Stripe/db, still refreshing the cache entry
with the live result.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lets the client force a live entitlement refetch right after a Stripe upgrade instead of waiting out the resolver's 60s cache. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agent route (GET /agent/entitlement) always calls resolveOrgEntitlement, which has no memoization of its own — it hits db.orgStripeCustomer + stripe.activeFeatureKeys live on every call, so the agent's forced refresh already never sees a stale entitlement post-upgrade. No production change needed for Slice U1's agent-route requirement; this test guards the assumption. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On the forked server's `{type:"entitlement"}` message, detect the
free→Pro transition (wasAutonomous false → canBeAutonomous true) and
fire `bumpAgentEntitlementRefresh()`. It stamps a fresh
`entitlementRefreshAt` ISO timestamp into each installed context's
assistant-config ConfigMap so every agent force-runs its entitlement
check on its next tick (bypassing the 12h throttle) instead of waiting
out its normal window. Per-context failures are isolated so one
unreachable cluster never blocks the others; the bump is fire-and-forget
so it never blocks the entitlement push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Thread an optional fresh flag from billingClient.entitlements through the entitlement provider so explicit refreshes hit GET /entitlements?fresh=1 and bypass the 60s server cache. refresh(fresh?) now returns the grace-applied current entitlement; the rigel:billing:refresh IPC handler passes fresh=1 and returns the payload to the renderer, as do the post-billing-window and sign-in refreshes. Boot + the 6h interval stay non-fresh to keep deduping Stripe calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… bump The agent reads the server-stamped `entitlementRefreshAt` from assistant-config each tick and tracks the last-honored value on assistant-state. When the marker changes, tick() passes `force` to determineEntitlement so it fetches immediately, bypassing the 12h throttle — unlocking premium capabilities within one tick of a free→Pro upgrade instead of at the next scheduled check. The agent stays authoritative: `force` means "fetch now", the backend still decides entitlement, and a failed forced fetch falls back to the normal grace/observe-only path (never crashes the tick). The honored marker is folded into tick's single end-of-tick writeState alongside the entitlement cache, so it persists exactly once per bump and can't clobber (or be clobbered by) that write. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single fixed 1.5s retry with a bounded, cancelable poll: on checkout completion, refetch entitlements up to 10 times with ~2s between attempts, stopping as soon as the plan flips to pro. A per-run cancel token (reset on unmount and when the modal closes) prevents refetches and setState after the modal is gone. Removes the race against Stripe's activation lag and the stale 60s server cache. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A small info banner above the embedded Stripe form sets expectations: Pro unlocks right away and the in-cluster agent switches on within ~30 seconds (its next entitlement check after the free->Pro edge stamps its config). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cloud contexts (GKE/EKS/AKS/DOKS) render faded with a lock badge in the tile's top-right corner when the org lacks cloudConnect; clicking a locked tile opens the upgrade flow instead of switching. Local clusters (kind/k3d/etc.) are unaffected. Reuses CLOUD_PROVIDERS for the classify + gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…users off cloud Connect-a-cluster modal: the four cloud provider tiles (DigitalOcean/EKS/GKE/AKS) render faded with a lock badge + a 'Pro' label when the org lacks cloudConnect; clicking one closes the modal and opens the upgrade. 'Import a kubeconfig' stays free. ClusterRail reconciliation is now lock-aware: on Free it never lands on or stays on a cloud context when a local one exists — it redirects to the first local cluster. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…symmetric agent drop Revocation from the Stripe dashboard happens out-of-app, so the desktop only learned on boot/6h. Now: re-fetch entitlement (fresh) whenever the window regains focus, and shorten the idle poll from 6h to 30m — so a canceled user loses Pro the moment they touch the app, or within 30m idle. Server-side, bump the agent's entitlementRefreshAt marker on ANY autonomy edge (not just free->Pro), so the in-cluster agent force-rechecks and drops its premium capabilities on downgrade too, symmetric with the instant upgrade. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wcase gate, cloud gating, propagation) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The backend has grown beyond waitlist capture into accounts, auth, billing and entitlements. Rename the service identity (directory, package name, build image, boot-log tags) to `api` to match its api.rigel.run domain. The signups feature (route, table, capture flow) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the Deployment/Service/Ingress/Secret objects rigel-signups -> rigel-api, the container + image + TLS secret, the build workflow (signups-build.yml -> api-build.yml, IMAGE ghcr.io/.../rigel-api), the release-images matrix entry and the docker-compose service. api.rigel.run host and default namespace unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Point RELEASING.md, the cut-release skill, and the desktop package-boundary comments at the renamed rigel-api image / api package. Signups feature copy and the signups endpoint/route references are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent path) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract classifyProvider, ProviderKind, CLOUD_PROVIDERS, and isCloudProvider into packages/k8s/src/provider.ts so the server can reuse them. clusterTile.ts re-exports the pure pieces (keeping the UI label/icon helpers); ClusterRail drops its inline CLOUD_PROVIDERS array for the shared isCloudProvider. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Free, block any context-scoped request whose resolved context classifies as
a cloud provider (GKE/EKS/AKS/DOKS) — regardless of how it entered the
kubeconfig, closing the hand-added-cluster bypass behind the client rail lock.
The gate sits right after the cloud connect/import routes and before the static
serve + every context-scoped /api/* route, returning 402 { gated: true }. The
/ws upgrade is rejected when the boot context is a locked cloud context. Exempts
health, contexts, cloud/*, and cluster delete/disconnect so a Free user can still
see and remove a locked cloud cluster. Context classification is cached 30s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ClusterOnboarding mounts ConnectClusterModal, which calls useUpgrade(). The provider only wrapped the main branch, so a zero-context first-run user white-screened when useUpgrade threw without a provider. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
payload null (entitlement IPC still resolving) read as LOCKED, so the reconciliation effect ran the destructive switchCluster(local) before Pro resolved. Gate isLocked on entitlementKnown and re-run on payload change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard setOrgStripeCustomer with `WHERE stripe_customer_id IS NULL` so a concurrent checkout can't overwrite an already-set customer, and re-read the stored customer after ensureCustomer so both concurrent sessions bind to the winner rather than an orphaned Stripe customer. Prevents a "paid but resolves as free" outcome when two checkouts race. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… only on success Two money-critical entitlement bugs: - Fix 1: cache fetchedAt using the agent's own clock at receipt, not the API server's timestamp. Clock skew stamped a future fetchedAt that tripped the grace/refetch future-rejection, refetching every tick, hitting the rate limiter, and flipping a paying org to free. - Fix 2: ack entitlementRefreshAt only after a successful forced fetch (a fresh cache was produced). A failed forced fetch now leaves the marker so the next tick retries, instead of silently degrading instant-upgrade to the 12h throttle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…zing state handleComplete polled ~10×2s (~20s) but the checkout banner promises ~30 seconds, then silently dropped back to the account view (often still Free), inviting a duplicate checkout. Poll 15×2s and, on exhaustion, show a 'still finalizing — use Refresh' note instead of a silent drop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sweep expired entries from the fixed-window limiter (at most once per window) so a flood of distinct keys can't grow memory without bound, and resolve the agent token to a known install BEFORE rate-limiting so random bearer tokens 401 without ever populating the map. Add a per-account cap to /entitlements (each call can fan out to Stripe) to protect the Stripe quota from an authed free account. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…upgrade The cloud-cluster usage gate only covered REST and a boot-context check at WS upgrade. WS frames honoring an explicit m.context bypassed it entirely (watch/logs/chat/action against a cloud cluster on Free), and the upgrade check used the boot context — not the app's active context — so a Free user whose default context was cloud had every WS upgrade destroyed, killing live data for all clusters. Remove the boot-context rejection at upgrade (always allow, keep session auth) and gate each frame by its resolved context: subscribe, logs.start, chat (including the fan-out read set) and action.run now drop a cloud context on Free with a typed gated error and skip the cluster call. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
tyrelchambers
marked this pull request as ready for review
July 16, 2026 18:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Design spec for the monetization foundation. Consolidates the two approved design records (entitlements architecture + org/teams model) with the product + billing decisions from brainstorming. Implementation plans follow after review.