Skip to content
Registry StackDocsDevelopment (unreleased)

Governed registry actions

View as Markdown

An immediate action is a named registry operation a caller invokes instead of writing records directly. Base Registry Engine (BReg) admits the action’s declared inputs, checks the grant that permits it, and applies a bounded set of writes in one transaction. A fixed-effect action maps admitted inputs onto declared effects. A handler action computes those effects in a Rhai script the project ships. Neither shape adds domain code to the server: an action stays configuration a reviewer can read, and the compiled contract covers its script, ABI, inputs, refusals, and write slots.

Reach for a fixed-effect action when the mapping from input to written field is constant, and for an acceptance-time requires check when the rule is a stored field of an existing reference target that must hold under the transaction’s lock. Neither needs a script.

A handler earns its place when a written value is computed from more than one input, when a branch decides which of the declared slots is written, or when the action needs a closed catalogue of business refusals that a caller can act on. A handler cannot widen what the action may do: it selects among slots the project already declared.

handler sits beside the action’s inputs and declares the script, its ABI, the refusal catalogue, and every slot the script may emit:

handler:
kind: rhai
script: scripts/register-person.rhai
abi: registry.action-handler/v1
refusals:
- {code: blank-name, label: At least one name part is required.}
writes:
- id: person
target: {entity: person}
operation: create
fields: [identifier, display-name]

fn handle(ctx) receives admitted values in ctx.inputs, keyed by logical input IDs, and returns exactly one of effects or refusal. A slot can be emitted once or omitted; its target, operation, and field ceiling come from writes, never from the script output. Rhai has no database query, stored-record snapshot, clock, filesystem, or network access under version 1.

Omitting a slot suppresses its write only. Every declared reference input still needs admission and compiled target authority, every compiled patch target still requires its condition token and ordered lock, and action requires checks stay unconditional for a new invocation.

A declared refusal returns 422 action.refused with the catalogue refusalCode, its static label as detail, and an optional fieldPath naming the public input. Dynamic messages and unknown codes are not accepted, and a refusal commits no effect, receipt, or event. An unexpected handler failure or invalid output returns 500 action.handler_failed, which an operator repairs in the project rather than in the request. Execution deadlines keep 503 service.unavailable.

registry.action-handler/v1 accepts scalar inputs, including references encoded as record-ID strings, and keeps decimal inputs in their canonical JSON string form. The compiler rejects crs84-point and structured inputs for this ABI; an action needing those types uses fixed effects. String and text inputs declare maxLength of at most 4,096 Unicode scalar values, which keeps every value inside Rhai’s 16,384-byte UTF-8 string budget. Admission rejects any input string above 16,384 bytes with 400 request.invalid before the script runs.

For this ABI an absent optional input stays absent, while explicit JSON null is a present key carrying Rhai’s unit value. Use in to tell the two apart before calling a string method. A fixed-effect action rejects explicit null instead.

registry.action-handler/v2 is a trial ABI that lets a project-level action call evidence::resolve(capability_id, subjects) inside handle(ctx). Version 1 input-only actions keep their existing path. The project declares each provider under evidenceProviders with an id, a reviewed client contracts file, and subjectResolution: trusted-provider-exact-selector; the imported contract becomes part of the signed package closure, so compilation and explanation need no network. Each action’s evidence entries then select a local id, that provider, an exact requirement URI, the subject role and profile, the output handles the script may read, and maximumObservationAgeSeconds.

Runtime configuration binds the same provider ID separately from the signed logical contract, under its own evidenceProviders map with baseUrl, trustBindingId, tokenRef, trustedJwksRef, revokedKeyIds, and an optional caBundleRef. The endpoint, token, and trusted JWKS are operator bindings that no script can replace. Computed identifiers stay request values: they cannot become authenticated claims or grants, and the action grant covers all declared processing, so a caller input selecting optional disclosure adds no authority.

The limits are fixed, and offline action explanation reports them:

LimitValueScope
Calls per declared capability1Each evidence entry
Declared capabilities2Each action
Concurrent evaluations8Each runtime
Response size256 KiBEach call
Retained Evidence1 MiBEach action invocation
Retention window24 hoursEach retained assertion
Assertion lifetime300 secondsEach signed response
Observation age1 to 300 secondsDeclared per capability

Accepted clock skew is zero. The whole invocation shares one deadline taken from operationalTimeouts.httpRequest, which defaults to 10,000 milliseconds and is never longer than the 30-second action ceiling. A failed Evidence dependency returns the static 503 problem, whose optional fieldPath names /evidence/{alias} for a known compiled capability and never carries selector values or provider response details.

The host admits the action and its existing targets before external work, releases the PostgreSQL connection, and evaluates Rhai once. Finalization rechecks authority, conditions, constraints, and evidence acceptance, then commits writes, audit, receipt, and the protected evidence-use material atomically. Successful receipt replay calls no provider.

An action that resolved Evidence retains the signed response and its verification context for 24 hours so a reviewer can see what the decision rested on. That is a separate retention scope from record history: ordinary history erasure does not cover it, and erase expired Evidence uses is the operator command that removes it once a window has closed.