Proposed. It settles the open rows of
@stackra/actions after wave 10 and touches
0099 (decision 2), 0138,
0212 (decision 4),
0215 and
0360
(decision 4). The amendments of 0137 and 0138 themselves are written by the ADR group. Linear
STA-765. Decision 5 amended by STA-813 (2026-10-26): an unbound approval step does not run; only
an action that declares approval: true fails closed.Context
Wave 10 left five gaps in the action pipeline. The effect tags existed as an enum in@stackra/actions/contracts
with snake_case codes while gen’s list and hints spelt external-communication in kebab case (ADR 0099 was written
before ADR 0231). The runtime recorded tool: false for every action while gen, following ADR 0360 decision 4, makes
an action a user can perform a tool unless it says tool: false with a reason. Plan limits were checked by the
entitlement step but nothing serialised two creates racing at the limit (owner round 3 item 18). The audit entry was
written after the unit of work committed, so a crash between the two lost it. And with no ApprovalGate bound the
approval step was skipped, so an app that forgot the binding ran every call unapproved.
Decision
- Effect codes are snake_case.
Effect(financial,security,pii,external_communication,irreversible,bulk) is the one vocabulary, exported from@stackra/actions/contracts(ADR 0231 wins over the kebab spelling of ADR 0099). An action declares its tags witheffects: ActionEffects(distinctEffectvalues); gen’s registered tags win at run time and reach the call descriptor. Gen’sEFFECT_TAGSand tool hints adopt the same codes. - The tool default is gen’s. At run time an action whose actors include a user is a tool unless it says
tool: false(with the// reason:line gen reads); any other action is not. A query stays a tool by default. - Reserve, then commit. A
LimitReservationport wraps every applied action’s unit of work: the driver (@stackra/entitlements, overLimitReservationService) holds the limit’s lock for a call that creates a counted model, checks the grant and the count, runs the commit and releases. Which call counts against which limit is the entitlements registry, never a verb the pipeline parses. No binding means no plan limits; callers write no reservation code. - The success audit is in the unit of work. A successful action’s audit entry is written inside its transaction,
after
handleand its usage event, so it commits or rolls back with the writes. A refusal’s entry (its writes rolled back) and an idempotent replay’s (no unit of work ran) are written after, outside. - Approval fails closed where it is declared (as amended by STA-813). The approval step runs only when an
ApprovalGateis bound; with none bound an app has no approvals and the step does not run. An action that declaresapproval: truemust pass a gate, so a call of it with no gate bound is an error, like a call with noAccessControl.OpenApprovalGateDriverremains for an app that binds a gate on purpose and wants every call to pass it. A query is never parked. The first text (every action fails closed with no gate) broke every pipeline built without one: the iam, entitlements and queue tests, the queue worker and the workflow runner.
Consequences
- A service whose tenants take approval rules binds the OS tasks-approvals driver; every other pipeline (the queue
worker, the workflow runner, the iam, entitlements and queue test suites) binds nothing and keeps working. The risk
that an app forgets the binding is carried by the actions that matter declaring
approval: true, which fail closed. - An action a user can perform appears in the tool table and at run time alike; a user-only action that must not be a tool says so with a reason.
- A long transaction now also holds the limit’s lock; the hold time is the service’s default (30 seconds).
- Gen’s effect list changes spelling; manifests regenerate.
Enforcement
packages/application/actions/src/core/services/__tests__/action-pipeline.service.test.ts (an unbound gate skips the step,
approval: true fails closed, reservation around the unit of work, audit order); packages/application/actions/src/core/bases/__tests__/action.base.test.ts
(tool default, effects); packages/security/entitlements/src/core/drivers/__tests__/plan-limit-reservation.driver.test.ts
(the entitlements LimitReservation driver, ADR 0408). Planned — the gen change of EFFECT_TAGS and its conformance
test.
Supersedes and sources
Supersedes: none. Sources: STA-765; owner round 3 items 18 and 33;notes/handover/stackra-package-needs.md.