-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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)"]
| 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 | 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.
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
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.
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"]
Activation fails with unsatisfiedRuntimeRequirement when a required provider, capability, role, or declaration is missing.
| 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.
| 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.
| 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. |
flowchart LR
Factory["ForsettiEntitlementProviderFactory.makeDefault"] --> Platform{"Runtime platform"}
Platform -- iOS --> StoreKit["StoreKit2EntitlementProvider"]
Platform -- macOS --> Static["StaticEntitlementProvider"]
Platform -- other --> Allow["AllowAllEntitlementProvider"]
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
StoreKit2EntitlementProvider ignores revoked and expired transactions. Restore purchases calls AppStore.sync() and then refreshes entitlement state.
| 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. |
- 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.iomatches 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.