go/entitlement.go
Snider ec423cfe46 feat: implement Section 21 — Entitlement permission primitive
c.Entitled("action", quantity) checks permission before execution.
Default: everything permitted (trusted conclave).
Consumer packages replace checker via c.SetEntitlementChecker().

- Entitlement struct: Allowed, Unlimited, Limit, Used, Remaining, Reason
- NearLimit(threshold), UsagePercent() convenience methods
- EntitlementChecker function type — registered by go-entitlements/commerce-matrix
- UsageRecorder for consumption tracking after gated actions succeed
- Enforcement wired into Action.Run() — one gate for all capabilities
- Security audit logging on denials (P11-6)
- 16 AX-7 tests including full SaaS gating pattern simulation

Maps 1:1 to RFC-004 EntitlementResult and RFC-005 PermissionResult.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 16:17:16 +00:00

130 lines
4 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
// Permission primitive for the Core framework.
// Entitlement answers "can [subject] do [action] with [quantity]?"
// Default: everything permitted (trusted conclave).
// With go-entitlements: checks workspace packages, features, usage, boosts.
// With commerce-matrix: checks entity hierarchy, lock cascade.
//
// Usage:
//
// e := c.Entitled("process.run") // boolean gate
// e := c.Entitled("social.accounts", 3) // quantity check
// if e.Allowed { proceed() }
// if e.NearLimit(0.8) { showUpgradePrompt() }
//
// Registration:
//
// c.SetEntitlementChecker(myChecker)
// c.SetUsageRecorder(myRecorder)
package core
import "context"
// Entitlement is the result of a permission check.
// Carries context for both boolean gates (Allowed) and usage limits (Limit/Used/Remaining).
//
// e := c.Entitled("social.accounts", 3)
// e.Allowed // true
// e.Limit // 5
// e.Used // 2
// e.Remaining // 3
// e.NearLimit(0.8) // false
type Entitlement struct {
Allowed bool // permission granted
Unlimited bool // no cap (agency tier, admin, trusted conclave)
Limit int // total allowed (0 = boolean gate)
Used int // current consumption
Remaining int // Limit - Used
Reason string // denial reason — for UI and audit logging
}
// NearLimit returns true if usage exceeds the threshold percentage.
//
// if e.NearLimit(0.8) { showUpgradePrompt() }
func (e Entitlement) NearLimit(threshold float64) bool {
if e.Unlimited || e.Limit == 0 {
return false
}
return float64(e.Used)/float64(e.Limit) >= threshold
}
// UsagePercent returns current usage as a percentage of the limit.
//
// pct := e.UsagePercent() // 75.0
func (e Entitlement) UsagePercent() float64 {
if e.Limit == 0 {
return 0
}
return float64(e.Used) / float64(e.Limit) * 100
}
// EntitlementChecker answers "can [subject] do [action] with [quantity]?"
// Subject comes from context (workspace, entity, user — consumer's concern).
type EntitlementChecker func(action string, quantity int, ctx context.Context) Entitlement
// UsageRecorder records consumption after a gated action succeeds.
// Consumer packages provide the implementation (database, cache, etc).
type UsageRecorder func(action string, quantity int, ctx context.Context)
// defaultChecker — trusted conclave, everything permitted.
func defaultChecker(_ string, _ int, _ context.Context) Entitlement {
return Entitlement{Allowed: true, Unlimited: true}
}
// Entitled checks if an action is permitted in the current context.
// Default: always returns Allowed=true, Unlimited=true.
// Denials are logged via core.Security().
//
// e := c.Entitled("process.run")
// e := c.Entitled("social.accounts", 3)
func (c *Core) Entitled(action string, quantity ...int) Entitlement {
qty := 1
if len(quantity) > 0 {
qty = quantity[0]
}
e := c.entitlementChecker(action, qty, c.Context())
if !e.Allowed {
Security("entitlement.denied", "action", action, "quantity", qty, "reason", e.Reason)
}
return e
}
// SetEntitlementChecker replaces the default (permissive) checker.
// Called by go-entitlements or commerce-matrix during OnStartup.
//
// func (s *EntitlementService) OnStartup(ctx context.Context) core.Result {
// s.Core().SetEntitlementChecker(s.check)
// return core.Result{OK: true}
// }
func (c *Core) SetEntitlementChecker(checker EntitlementChecker) {
c.entitlementChecker = checker
}
// RecordUsage records consumption after a gated action succeeds.
// Delegates to the registered UsageRecorder. No-op if none registered.
//
// e := c.Entitled("ai.credits", 10)
// if e.Allowed {
// doWork()
// c.RecordUsage("ai.credits", 10)
// }
func (c *Core) RecordUsage(action string, quantity ...int) {
if c.usageRecorder == nil {
return
}
qty := 1
if len(quantity) > 0 {
qty = quantity[0]
}
c.usageRecorder(action, qty, c.Context())
}
// SetUsageRecorder registers a usage tracking function.
// Called by go-entitlements during OnStartup.
func (c *Core) SetUsageRecorder(recorder UsageRecorder) {
c.usageRecorder = recorder
}