HIPsHanzo Proposals
Back to HIPs
HIP-1202FinalStandards TrackPlatform

Entitlement — What an Org May Run

Hanzo AI
Created: 2026-08-20
Requires: HIP-0106, HIP-0118, HIP-0139

HIP-1202: Entitlement — What an Org May Run

Abstract

entitlement answers what your org may run: which products the plan grants, and which of those the org has switched on. It is apps/entitlement in hanzoai/cloud, and its whole discipline is that those are two authorities, never braided — ENTITLEMENT is the billing truth, read from commerce at decision time; ENABLEMENT is the org's intent, the one store this capability owns (apps/entitlement/entitlements.go:1-33).

Motivation

A paywall whose grant check and toggle store are one table cannot say whether an org stopped paying or switched something off, and every consumer grows its own merge of the two. Keeping the billing truth in commerce and only the intent here means enabling never spends new money — a plan upgrade happens in commerce, not here — and disabling is never gated.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

§1 Addresses

operationwhat
GET /v1/entitlementthe caller's plan projection: { tier, apps: {…} }, the per-app booleans the console shell renders (apps/entitlement/projection.go)
GET /v1/entitlement/orgs/{org}{ "enabled": [...] } — the org's toggled-on products
POST /v1/entitlement/orgs/{org}{ "add": [...], "remove": [...] } → { "enabled": [...] }, bounded at 64 products per batch (apps/entitlement/entitlements.go:59-61)

All three are typed operations. Today's router serves the org pair at /v1/orgs/{org}/entitlements, under a root this capability does not own; that pair is carried by hanzoai/cloud openapi/misfiled.txt and closes by fold, mirroring the /v1/kms/orgs/{org} shape the key service already serves.

§2 Store

One encrypted SQLite database, the deployment's own entitlement, opened through the one opener (sqlpool.Open("entitlements", dir), apps/entitlement/store.go:47) and keyed (org, product). Product ids and org labels are validated at the edge against the one product-id and org-label shapes (apps/entitlement/entitlements.go:63-72).

§3 Tenancy

{org} MUST equal the caller's validated org (c.Org()), unless the caller is a SuperAdmin — c.IsAdmin(), minted only for owner == "admin" and never client-forgeable — who may target any org (HIP-0118). A bearer-less request with a restored X-Org-Id and no verified user fails the principal.Validated gate and is refused 403 (apps/entitlement/entitlements.go:26-32). There is no path by which one org reads or writes another's entitlements.

§4 The gate, and its two fail directions

A product may only be ENABLED if it is ENTITLED: the write reads commerce.CheckEntitlement and a non-SuperAdmin can only switch on what the org already pays for; a SuperAdmin bypasses the gate — the operator can comp any product to any org. With commerce unreachable, a non-SuperAdmin enable fails closed, 503, never open (apps/entitlement/entitlements.go:74-77). DISABLING is always allowed.

The two read paths fail in deliberately opposite directions (apps/entitlement/projection.go:9-17): the UI projection fails safe-to-locked — commerce down means an app reports false at 200, never a 5xx — while the enforcement leg (RequireProduct, apps/entitlement/require.go) fails open, so functionality is preserved during a billing outage even as the UI conservatively shows locked. The plan → product policy itself lives once, in the catalog commerce resolves; it is never restated here.

§5 Metering, events, observability, stage

The capability is free (plugin/entitlement/main.go:22, cloud.Free). It publishes no events on the bus and delivers nothing to customer webhooks. Beyond the request span, it emits its mount line and one audit-shaped log per mutation — org, add, remove, whether SuperAdmin, and the actor (apps/entitlement/entitlements.go:306). Its stage is ga — the manifest row (manifest/apps.go:366) carries no stage.

§6 Upstream

The capability derives from no forked, embedded or mirrored OSS project. Its one non-standard-library import beyond the app framework is github.com/hanzoai/account v0.3.3 (MIT OR Apache-2.0), the billing-account rule as a library.

Rationale

The alternative to the opposite fail directions is one posture for both reads. Failing everything closed turns a commerce outage into a product outage; failing everything open renders upgrade UI as if every org owned every plan. Splitting by consequence — enforcement preserves function, projection preserves honesty — costs one paragraph of doctrine and buys an outage that degrades instead of cascading.

Security Considerations

The wrong implementation grants product for free or across tenants. Skipping the entitlement leg on enable turns the toggle store into a self-serve comp system; the commerce read at write time is the control, and its fail-closed posture for non-SuperAdmins is what keeps an outage from becoming a giveaway. Trusting a per-org isAdmin instead of the one SuperAdmin predicate is the privilege escalation HIP-0118 names. The org label folded into the store key is validated strictly at the edge, so a crafted {org} cannot address another tenant's rows.

References

  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-0118 — SuperAdmin & Tenant Isolation Model
  • HIP-0139 — Capability

Copyright

Released under CC0 1.0 Universal Public Domain Dedication.