Skip to content

Entitlements and Capabilities

James Daley edited this page Jun 18, 2026 · 3 revisions

Entitlements and Capabilities

Forsetti evaluates module access in layers. Capabilities describe what a module is allowed to ask for. Entitlements decide whether the module is unlocked. Runtime requirements prove that declared needs can actually be satisfied before lifecycle code starts.

Gate Stack

flowchart TD
    Manifest["ModuleManifest"] --> Compatibility["CompatibilityChecker"]
    Compatibility --> Schema["schemaVersion and manifestTemplateVersion"]
    Compatibility --> Platform["supportedPlatforms"]
    Compatibility --> Version["Forsetti version range"]
    Compatibility --> Policy["CapabilityPolicy"]
    Policy --> Report{"Compatible?"}
    Report -- no --> Block["Block activation"]
    Report -- yes --> Entitlement["EntitlementProvider.isUnlocked"]
    Entitlement --> Unlocked{"Unlocked?"}
    Unlocked -- no --> Locked["moduleLocked"]
    Unlocked -- yes --> Context["Build capability-scoped context"]
    Context --> RuntimeReq["Validate runtime requirements"]
    RuntimeReq --> UIReq["Validate UI contribution declarations"]
    UIReq --> Start["start(context)"]
Loading

Capability Policy

Policy Behavior
AllowAllCapabilityPolicy Allows requested capabilities. Useful for tests, local development, and simple hosts.
FixedCapabilityPolicy Allows only the configured capability set. Useful for locked-down hosts and focused tests.
Custom policy Implement CapabilityPolicy.evaluate(moduleID:capability:) for product-specific governance.

CompatibilityChecker reports capabilityDenied when the selected policy denies a requested capability. The checker no longer applies a blanket denial to ui_theme_mask; however, the default ModuleManager still strips theme masks before publishing UI surface state because host shell theming remains controlled by the host.

Capability Families

Capability Raw Value Runtime Surface
networking networking NetworkingService
storage storage StorageService
secureStorage secure_storage SecureStorageService
fileExport file_export FileExportService
cryptoUtilities crypto_utilities Cryptographic utilities when provided by the host.
telemetry telemetry TelemetryService
routingOverlay routing_overlay Overlay schema and route/pointer publication.
uiThemeMask ui_theme_mask Theme-mask declaration and validation; default publication is sanitized.
toolbarItems toolbar_items Toolbar item publication.
viewInjection view_injection Slot-based SwiftUI view injection.
sharedDatabase shared_database SharedDatabaseService or a default-role service module.
authentication authentication AuthenticationService or a default-role service module.
diagnostics diagnostics DiagnosticsService or a default-role service module.
api api APIService or a default-role service module.
security security SecurityService or a default-role service module.

Unknown service types are denied by default by CapabilityScopedServiceProvider. Denied service resolution is logged as a warning and returns nil.

Scoped Service Resolution

sequenceDiagram
    participant Module as Module
    participant Context as ForsettiContext
    participant Scoped as CapabilityScopedServiceProvider
    participant Host as Host ServiceContainer
    participant Log as ForsettiLogger

    Module->>Context: services.resolve(TelemetryService.self)
    Context->>Scoped: resolve(TelemetryService)
    Scoped->>Scoped: map service type to telemetry capability
    alt capability granted
        Scoped->>Host: resolve(TelemetryService)
        Host-->>Module: service or nil
    else capability missing or unmapped
        Scoped->>Log: warning
        Scoped-->>Module: nil
    end
Loading

The module can publish events and send module messages only as its own bound module ID. Attempts to spoof another module ID are rejected by the scoped context.

Runtime Requirement Enforcement

flowchart TD
    Manifest["Module manifest"] --> IO["runtimeRequirements.io"]
    Manifest --> Data["runtimeRequirements.dataIsolation"]
    Manifest --> Role["defaultModuleRole"]
    IO --> CapCheck["Each I/O kind requires matching capability"]
    IO --> ProviderCheck["Required I/O requires available provider"]
    Data --> Shared{"framework_mediated_shared?"}
    Shared -- yes --> SharedProvider["shared_database provider must exist"]
    Data --> RoleDeps["requiredDefaultRoles must be available"]
    Role --> RoleType["Role must match moduleType"]
    Role --> RoleCap["Service roles require matching capability"]
Loading

Activation fails with unsatisfiedRuntimeRequirement when a required provider, capability, role, or declaration is missing.

Default Provider Roles

Role Valid Provider Module Required Capability Consumer Dependency
ui ui or app none Host can identify a default UI/app module.
shared_database service shared_database Consumers can require shared database availability.
authentication service authentication Consumers can require authentication availability.
diagnostics service diagnostics Consumers can require diagnostics availability.
api service api Consumers can require API provider availability.
security service security Consumers can require security provider availability.

Default roles are manifest-level contracts. They do not create services automatically; the host still owns concrete service adapters and module registry wiring.

UI Contribution Enforcement

Contribution Required Capability Additional Template 1.1 Declaration
Toolbar items toolbar_items runtimeRequirements.ui.toolbarItemIDs
View injections view_injection runtimeRequirements.ui.viewIDs and slotIDs
Overlay schema routing_overlay runtimeRequirements.ui.routeIDs and pointerIDs
Theme mask ui_theme_mask runtimeRequirements.ui.themeIDs; validated, then stripped by default publication.

Template 1.1 UI/app modules with any UI contributions must include runtimeRequirements.ui. Undeclared contribution IDs fail activation before the surface manager is updated.

Entitlement Providers

Provider Typical Use
AllowAllEntitlementProvider Free modules, tests, local prototypes, or debug bypass.
StaticEntitlementProvider macOS defaults, test fixtures, local unlock simulation, or non-StoreKit products.
StoreKit2EntitlementProvider iOS production StoreKit entitlement checks.
Custom provider Enterprise licensing, server-backed entitlement state, or alternate commerce flows.

Default Provider Factory

flowchart LR
    Factory["ForsettiEntitlementProviderFactory.makeDefault"] --> Platform{"Runtime platform"}
    Platform -- iOS --> StoreKit["StoreKit2EntitlementProvider"]
    Platform -- macOS --> Static["StaticEntitlementProvider"]
    Platform -- other --> Allow["AllowAllEntitlementProvider"]
Loading

StoreKit Flow

sequenceDiagram
    participant Runtime as ForsettiRuntime
    participant Provider as StoreKit2EntitlementProvider
    participant StoreKit as StoreKit
    participant Manager as ModuleManager

    Runtime->>Provider: refreshEntitlements()
    Provider->>StoreKit: Transaction.currentEntitlements
    StoreKit-->>Provider: verified active products
    Manager->>Provider: isUnlocked(moduleID, productID)
    Provider-->>Manager: true or false
    StoreKit-->>Provider: Transaction.updates
    Provider-->>Runtime: entitlementsDidChangeStream event
Loading

StoreKit2EntitlementProvider ignores revoked and expired transactions. Restore purchases calls AppStore.sync() and then refreshes entitlement state.

Product ID Rules

Manifest Field Result
iapProductID omitted or null Module is treated as unlocked.
iapProductID set and provider reports product unlocked Module can pass entitlement gate.
iapProductID set and provider reports product locked Activation fails with moduleLocked.

Review Checklist

  • Requested capabilities match actual service, UI, routing, and telemetry behavior.
  • Paid modules have stable product IDs before release.
  • Debug entitlement bypass cannot leak into production configuration.
  • Capability denials are covered by tests when custom policies are used.
  • Entitlement changes deactivate modules that are no longer unlocked.
  • Default-role provider modules are service modules unless the role is ui.
  • runtimeRequirements.io matches requested capabilities and provider expectations.
  • UI contribution IDs are declared under runtimeRequirements.ui.
  • Theme-mask declarations are not treated as host shell styling until the host explicitly supports them.

Clone this wiki locally