83 lines
6.4 KiB
Markdown
83 lines
6.4 KiB
Markdown
# 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 until `request_expires_at`.
|
|
- `APPROVED`: a different authorized actor approved it; the evidence remains executable until `expires_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`, or `COMPENSATE`);
|
|
- 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:
|
|
|
|
1. Verify the immutable Transition plan and resolve the effective mode and trusted contributions.
|
|
2. Resolve the current route and require an exact request identity.
|
|
3. Expire stale requests and lock the selected approval.
|
|
4. Require `APPROVED`, unexpired, complete evidence with the exact mode, fingerprints, environment, and Executor.
|
|
5. Copy the complete requester and approver snapshot into the immutable execution audit row.
|
|
6. Insert the execution batch and atomically update the approval to `CONSUMED` with its execution ID.
|
|
7. 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=true`
|
|
- `FACTORY_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.
|
|
|