Skip to main content
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

  1. 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 with effects: ActionEffects (distinct Effect values); gen’s registered tags win at run time and reach the call descriptor. Gen’s EFFECT_TAGS and tool hints adopt the same codes.
  2. 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.
  3. Reserve, then commit. A LimitReservation port wraps every applied action’s unit of work: the driver (@stackra/entitlements, over LimitReservationService) 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.
  4. The success audit is in the unit of work. A successful action’s audit entry is written inside its transaction, after handle and 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.
  5. Approval fails closed where it is declared (as amended by STA-813). The approval step runs only when an ApprovalGate is bound; with none bound an app has no approvals and the step does not run. An action that declares approval: true must pass a gate, so a call of it with no gate bound is an error, like a call with no AccessControl. OpenApprovalGateDriver remains 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.