Appearance
Operations & the scope host
The scope-host contract is the heart of the kernel: module code registers operations; callers reach a scope only through a capability stub. The operation handler runs inside the scope's execution domain, which is what makes invariants enforceable — the handler sees sql, emit, check, and link; the caller sees only invoke().
The contract
ts
interface ScopeHost {
getScope(principal: PrincipalId, tenantId: TenantId, scopeId: ScopeId): Promise<ScopeStub>;
provisionScope(input: ProvisionScopeInput): Promise<void>;
registerModule(registration: ModuleRegistration): void;
defineOperation<I, O>(name: string, handler: OperationHandler<I, O>): void;
readonly admin: HostAdmin; // roles, assignments, grants, membership
close(): Promise<void>;
}
interface ScopeStub {
readonly tenantId: TenantId;
readonly scopeId: ScopeId;
invoke<O, I>(operation: string, input?: I): Promise<O>;
}Operation names are module-namespaced: 'workorder/create', 'invoicing/export'.
What a handler sees
ts
interface OperationContext {
readonly tenantId: TenantId; // ambient — from the stub, not from the caller
readonly scopeId: ScopeId;
readonly principal: PrincipalId;
readonly sql: ScopedSql; // synchronous, scope-local SQL
emit(event: DomainEventInput): void;
check(permission: PermissionKey, entity?: EntityRef): Promise<Decision>;
link(child: EntityRef, parent: EntityRef): void;
}sqlqueries the scope's own database — synchronously, because the data is local to the execution domain. One network hop to reach the scope, then local queries.emitvalidates the event input and stamps the envelope kernel-side (id, timestamp, tenant, scope, actor). See Events & audit.checkasks the permission checker about the ambient principal at the ambient node, optionally narrowed to one entity. See Permissions.linkrecords a child→parent relation tuple (e.g. work order → facility) used by the permission evaluator's entity-edge rule. The relation must be declared in a registered module'sentityRelations. Idempotent.
Contract semantics — what every adapter guarantees
These are the semantics the conformance suite verifies, so you can rely on them regardless of which adapter is underneath:
Strict serialization per scope
One operation at a time, to completion. Ten concurrent read-await-write increments land on exactly ten. Module code never needs locks, transactions-for-concurrency, or retry loops against its own scope.
Structured-clone boundary
Inputs and results are cloned on every stub call, both directions — even in-process. Mutating an input object after invoke(), or mutating a returned result, can never affect scope state. Code cannot share mutable state with a scope, so "it worked locally because we shared memory" bugs are impossible by construction.
Fail-closed addressing
getScope validates (tenantId, scopeId) against the directory. A mismatched pair throws; it never resolves to another tenant's scope.
Kernel-stamped events
The event envelope's origin fields are not parameters. See Events & audit.
In-scope functions vs registered operations
Engines expose their logic at two altitudes:
- Registered operations (
'workorder/create') — the default bindings, each starting with its own permission check. Invoke these through a stub. - In-scope functions (plain exports like
createWorkOrder(ctx, input)) — composable building blocks a vertical's own operation can call in the same transaction, when it needs to wrap engine behavior with domain logic (pricing, extra validation). The caller is then responsible for the permission check.
This is how a vertical customizes without forking: write your own operation, call the engine's in-scope functions, keep everything inside one serialized, audited execution.
ts
host.defineOperation('acme/create-priced-workorder', async (ctx, input) => {
assertAllowed(await ctx.check(PERM.create));
const order = createWorkOrder(ctx, toEngineInput(input)); // engine function
ctx.sql.exec('INSERT INTO acme_pricing ...'); // vertical's own table
return order;
});Event consumers
A module can subscribe to event types (declared in its manifest under events.consumes). Consumers run as ordinary in-scope operations under a system actor, with at-least-once delivery tracked in a kernel delivery journal — so handlers must be idempotent. Ordering is guaranteed only within one (scope, module) pair.
The invoicing engine is the reference example: it consumes workorder.completed and rebuilds its own state from the event payload alone.