Skip to content

HELM-16 monetization foundation (spec) - #53

Merged
tyrelchambers merged 94 commits into
masterfrom
feature/helm-16-monetization
Jul 16, 2026
Merged

HELM-16 monetization foundation (spec)#53
tyrelchambers merged 94 commits into
masterfrom
feature/helm-16-monetization

Conversation

@tyrelchambers

Copy link
Copy Markdown
Owner

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.

tyrelchambers and others added 30 commits July 15, 2026 12:03
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>
tyrelchambers and others added 28 commits July 16, 2026 07:48
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
tyrelchambers marked this pull request as ready for review July 16, 2026 18:35
@tyrelchambers
tyrelchambers merged commit 6d6f42b into master Jul 16, 2026
1 check 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.

1 participant