6.4 KiB
Plugin Separation-of-Duties Approval Design
Context
P2-D8e2d bound a real execution to the Transition fingerprint, routing fingerprint, and environment, but the execution operator supplied the approval reason in the same request. That evidence was durable, yet it did not represent an independent decision and had no pending lifetime, approved lifetime, or revocation lifecycle.
P2-D8e2g replaces that shortcut with persistent approval requests. A real execution can consume only one independently approved record whose complete identity still matches the current Transition and routing policy. Dry-run execution remains approval-free.
Lifecycle
factory_plugin_transition_approval owns five terminal or active states:
PENDING: an authorized requester submitted a bounded reason; the request remains decidable untilrequest_expires_at.APPROVED: a different authorized actor approved it; the evidence remains executable untilexpires_at.REVOKED: an approver-role actor revoked a pending or approved request with a reason.CONSUMED: one execution batch atomically claimed the evidence and stored its execution ID.EXPIRED: the pending or approved lifetime elapsed, or a sibling request became obsolete after another approval was consumed.
Expiry is applied lazily under the Transition row lock before list, decision, revocation, and execution operations. State-changing SQL also repeats status and time predicates, so a stale in-memory decision cannot overwrite a concurrent terminal state.
Frozen Identity
Every request freezes:
- Transition ID and SHA-256 fingerprint;
- effective execution mode (
APPLY,RETRY, orCOMPENSATE); - routing configuration fingerprint;
- execution environment and summary Executor code;
- requester account, canonical sorted role snapshot, reason, request time, and request expiry.
Approval adds the approver account, canonical role snapshot, decision reason, approval time, and evidence expiry. Routing identity now includes separation policy, both TTL values, and canonical requester/approver role allowlists. Any of those configuration changes invalidates unconsumed evidence.
The execute request accepts only an approvalId plus the exact Transition, routing, and environment identity. The old direct approvalReason request field and routing-policy self-approval helper were removed.
Actor And Role Policy
PluginApprovalPolicy requires a valid authenticated account and one to 50 lowercase role keys for every approval actor. Role keys are deduplicated, sorted, and frozen in a comma-separated snapshot bounded to 1,000 characters.
Two optional allowlists constrain who may request and approve. Empty lists preserve permission-based compatibility; a non-empty list requires at least one matching current role. With separation enabled by default, requested_by and approved_by must differ both at decision time and again at execution consumption time.
The independent API permission is generator:plugin:approve. Request and execution operations retain generator:plugin:execute; approve and revoke require the new permission; either permission may inspect approval history. Server-side account and role checks remain authoritative regardless of frontend visibility.
Locking And Consumption
All lifecycle operations lock the owning Transition first. Decisions and execution then lock the approval row, preserving one lock order. Execution performs the following work in one local transaction:
- Verify the immutable Transition plan and resolve the effective mode and trusted contributions.
- Resolve the current route and require an exact request identity.
- Expire stale requests and lock the selected approval.
- Require
APPROVED, unexpired, complete evidence with the exact mode, fingerprints, environment, and Executor. - Copy the complete requester and approver snapshot into the immutable execution audit row.
- Insert the execution batch and atomically update the approval to
CONSUMEDwith its execution ID. - Expire active sibling approvals, then create step receipts and the Outbox command.
If consumption loses a race, the transaction fails before receipts or Outbox creation. Once an execution has consumed evidence, revocation is rejected. The target-side effects still occur asynchronously through the existing leased Outbox.
API And UI
The Plugin API adds request, list, approve, and revoke endpoints below each Transition. Registry and delivery status expose only non-secret policy settings: separation enabled, request TTL, approved-evidence TTL, and role allowlists where relevant.
The Plugin center provides a dedicated approval table. It shows lifecycle status, effective mode, requester and approver timestamps, reasons, role snapshots, expiry, revocation, consumption, and both fingerprints. A real execution no longer opens a combined approval-and-execute prompt. It searches for an approved, unexpired record matching the visible Transition, route, environment, and effective retry/compensation mode; otherwise it opens the approval table. Execution history retains the consumed evidence snapshot after the source approval later becomes terminal.
Configuration
The defaults are environment-overridable:
FACTORY_PLUGIN_APPROVAL_SEPARATION_ENABLED=trueFACTORY_PLUGIN_APPROVAL_REQUEST_TTL_MINUTES=1440(bounded to 1..10080)FACTORY_PLUGIN_APPROVAL_VALIDITY_MINUTES=30(bounded to 1..1440)FACTORY_PLUGIN_APPROVAL_REQUESTER_ROLE_KEYS=FACTORY_PLUGIN_APPROVAL_APPROVER_ROLE_KEYS=
The existing FACTORY_PLUGIN_APPROVAL_REQUIRED_FOR_NON_DRY_RUN switch remains the outer real-execution gate.
Compatibility And Boundaries
- Existing execution rows receive nullable/defaulted audit columns through the MySQL 5.7-compatible upgrade script. New full schemas create the approval table before new executions are accepted.
- Approval evidence is an application database record, not a cryptographic signature, external ticket, or identity-provider attestation.
- The UI conservatively opens approval workflow when Registry status reports real Executors; the service resolves each Transition's actual contributions and remains the final authority.
- This stage does not connect to or mutate an external MySQL target. SQL, permission, and menu target behavior continues to be covered by isolated H2 MySQL-mode rehearsals until a preview target is explicitly authorized.
- Plaintext development credentials and TLS verification remain deferred by explicit user decision.