Skip to content

Operations, functions & permissions

An engine has no endpoints. It exposes operations (invoked through a scope stub) and in-scope functions (called by a vertical inside its own transaction). HTTP, where it exists, is a generated artifact pointed at by the manifest's api field.

This engine has a third surface the others don't: it contributes a guard predicate.

Operations

OperationPermissionDoes
protocol/define-templateprotocol:createregister a new template version
protocol/list-templatesprotocol:readlist registered templates
protocol/instantiateprotocol:createstart an instance on an entity
protocol/fillprotocol:fillappend a response (open checklist instances only)
protocol/bind-documentprotocol:bindbind vertical content (ref, hash) to an open document instance
protocol/request-signaturesprotocol:request-signaturefreeze and send to named parties for external signature
protocol/cancel-signaturesprotocol:request-signaturewithdraw an outstanding request set and thaw
protocol/record-signatureprotocol:record-signaturerecord a signature a provider reported
protocol/decline-signatureprotocol:record-signaturerecord a refusal or an expiry
protocol/signprotocol:signsign in-app; freezes the instance forever
protocol/countersignprotocol:countersignadd a second signature on frozen content
protocol/voidprotocol:voidsupersede an instance (never deletes)
protocol/getprotocol:readone instance with responses and signatures
protocol/list-for-entityprotocol:readevery protocol on an EntityRef

In-scope functions

The most complete composable surface of any engine here — every operation has a function behind it:

ts
import { instantiateProtocol, requireSigned, PROTOCOL_PERM } from '@substrat-run/engine-protocol';
FunctionNotes
defineTemplate(ctx, input)register a (key, version)
listTemplates(ctx)
instantiateProtocol(ctx, input)binds to any EntityRef
fillProtocol(ctx, input)append-only; checklist kind only; throws once frozen
bindDocument(ctx, input)document kind only; rebindable while open
requestSignatures(ctx, input)async — freezes content, opens a request per party
recordSignature(ctx, input)async — an external signatory, at the provider's time
declineSignature(ctx, input)a refusal or expiry; does not thaw the instance
cancelSignatureRequests(ctx, input)withdraws the set and thaws back to open
signProtocol(ctx, input)async — in-app; computes the content hash via Web Crypto
countersignProtocol(ctx, input)async — recomputes and compares the hash
voidProtocol(ctx, input)
getProtocol(ctx, instanceId)
listProtocolsForEntity(ctx, entity)
protocolContentHash(ctx, …)async — the hash recipe, exported so you can verify independently
requireSigned(ctx, entity, templateKey)throws unless signed — the completion-guard building block
requireCountersigned(ctx, …)the counter-signed equivalent

There is deliberately no requireAllSigned. In the request-driven path an instance reaches signed only once every requested party has signed, so "all parties signed" isrequireSigned — a second predicate would read as though it checked something stronger while checking the same thing. The multi-party question is answered by the state machine.

None of these check permissions — that is the caller's job.

signProtocol, countersignProtocol, requestSignatures, recordSignature and protocolContentHash are async because Web Crypto is; the rest are synchronous. That protocolContentHash is exported at all is the point of the invariant: the recipe is a contract you can replay, not a black box.

The guard predicate

ts
predicates: { 'protocol/all-signed': allSignedPredicate }

The engine contributes the predicate; a vertical manifest wires it. The engine declares no guard of its own — what is mandatory when is vertical policy, and the engine cannot know another module's operations.

This is the only genuinely config-shaped surface in any engine. Its config is declared in the vertical's manifest, kernel-opaque, and parsed by the predicate that owns it:

ts
export const allSignedGuardConfig = z.object({
  templateKey: z.string().min(1),   // vertical content: 'tillstandsrapport'
  entityType: z.string().min(1),    // what the protocol hangs on: 'workorder'
  entityIdFrom: z.string().min(1),  // input field holding the id: 'orderId'
  countersigned: z.boolean().default(false),
});

entityIdFrom is the trick worth stealing: it late-binds into the vertical's vocabulary, so one predicate serves any operation shape without the engine ever knowing the vertical's words. That's how you parameterise engine behaviour without a config bag — see Composing.

Permissions

KeyDescription
protocol:createDefine protocol templates and start protocol instances on entities
protocol:fillRecord responses on an open checklist protocol (append-only)
protocol:bindBind vertical-owned document content (ref + hash) to an open document protocol
protocol:request-signatureFreeze and request signatures from named parties; also cancels a pending set
protocol:record-signatureRecord a signature reported by an external provider
protocol:signSign in-app — freezes it forever (the technician fills, the arbetsledare signs)
protocol:countersignCounter-sign an already-signed protocol — a second signature on the same frozen content
protocol:readRead protocol templates, instances, responses, signature requests and signatures
protocol:voidVoid (supersede) a protocol — never deletes

Nine keys for what looks like one workflow, because the separations are the product: fill ≠ sign is the arbetsledare rule, sign ≠ countersign is the customer at pickup, and void is a supervisory act.

protocol:record-signature is the odd one and deliberately so: it speaks for an external provider, not for a person, and it is held by no human role in any demo. A staff signer must not be able to assert that some customer signed with BankID. When ingress lands (#96, #97), granting this key is how a deployment declares what it trusts to speak for a provider — which is why it belongs in the permission diff rather than in a config file.

Entitlement

entitlementKey: 'protocol'. Checked per invoke, fails closed. A binary SKU gate, not configuration.

The hard parts, hosted.