Files
yidaima/RuoYi-Vue/docs/superpowers/specs/2026-07-11-plugin-menu-target-executor-design.md

108 lines
7.8 KiB
Markdown

# Plugin Menu Target Executor Design
## Context
P2-D8e2e delivered `permission-jdbc:v1`, but its execution context did not carry the environment frozen in the approved execution batch. The JDBC adapters trusted only the locally configured target environment, so an operator could approve one environment while a changed connection configuration pointed at another. Menu contributions also had no real target adapter.
P2-D8e2f closes both gaps. Every JDBC adapter now requires the approved execution environment to exactly equal the isolated JDBC target environment before opening a connection. A new `menu-jdbc:v1` adapter then installs one narrow RuoYi page menu, records target ownership, grants existing roles, and conservatively compensates only resources it owns.
## Approved Environment Gate
`PluginTransitionOutboxWorker` copies `PluginTransitionExecution.environmentCode` into every `PluginContributionExecutionContext`. `PluginJdbcTargetEnvironmentPolicy` compares that immutable value with `factory.plugin-execution.sql-target.environment-code`.
The comparison is exact and happens before a JDBC connection is opened. A blank legacy value, a different environment, or a changed target configuration fails with a sanitized environment-mismatch error. `sql-jdbc:v1`, `permission-jdbc:v1`, and `menu-jdbc:v1` all share this policy.
## Menu Document
Each `MENU + admin_frontend` contribution contains exactly one JSON document:
```json
{
"schemaVersion": "1.0",
"menuKey": "menu:delivery-verification",
"displayName": "Delivery verification",
"parentPath": "generator",
"path": "deliveryVerification",
"component": "generator/delivery/index",
"routeName": "DeliveryVerification",
"icon": "check",
"orderNum": 16,
"requiredPermissionCode": "generator:delivery:verify",
"roleKeys": ["common"]
}
```
Validation is strict:
- only the published fields are accepted and the document is bounded to 64 KiB;
- `schemaVersion` must be `1.0` and `menuKey` must equal the immutable contribution key;
- parent and child paths are bounded route segments and cannot be equal;
- the component is a relative repository-style path ending in `/index`; traversal and URLs are rejected;
- route name, icon, order number, and display name have explicit formats and size limits;
- the required permission is a three-part lowercase permission identifier with no wildcard;
- one to 20 unique lowercase role keys are allowed.
Role keys are sorted before computing the semantic SHA-256. Apply and rollback resources may differ in formatting or role order, but must describe the same menu identity.
## Permission Dependency
A page menu does not duplicate the required permission in its own `sys_menu.perms` field. The page row uses an empty permission value, while `requiredPermissionCode` must resolve to an `ACTIVE` row in `factory_plugin_target_permission` whose target menu still exists as an enabled `F` button with the same permission code.
This preserves the permission adapter's unique permission identity and makes dependency order explicit. The menu adapter locks the menu ownership row before the permission ownership row in both apply and compensation, avoiding reverse lock order. The first menu Plugin also declares a manifest dependency on `delivery.preview-permission@^1.0.0`.
## Apply Semantics
After claiming the generic target receipt lease, the adapter runs one target transaction:
1. Resolve one enabled parent menu by stable `parentPath`.
2. Lock any existing menu ownership row.
3. Lock and verify the active factory-owned permission dependency.
4. Resolve every declared active role key and require both the complete parent-menu ancestry grant and the required permission grant.
5. Reject unowned path or route-name collisions.
6. Create one enabled RuoYi page menu (`menu_type = 'C'`) with fixed platform-owned field semantics.
7. Persist Plugin/version/contribution identity, semantic document fingerprint, resolved parent and permission IDs, generated menu ID, and `ACTIVE` status.
8. Insert only the declared role grants and matching ownership rows.
An already active ownership row is treated as a replay only after every immutable field, route identity, target menu field, role ID, owned grant, parent ID, and permission dependency has been revalidated. Drift is never overwritten.
## Compensation Semantics
Compensation validates the complete target state before deleting anything. It refuses when:
- Plugin owner, contribution key, version, or semantic document identity differs;
- the parent menu or required permission has moved, disappeared, or become inactive;
- the page menu route or managed fields have drifted;
- any owned role grant is missing;
- a declared role loses parent-menu ancestry or the required permission grant;
- an unmanaged role grant or child menu has been added.
On success, one transaction deletes only owned role grants and the owned page row, clears active role ownership rows, and marks the menu ownership `REMOVED`. The tombstone supports duplicate compensation and later reactivation by the same Plugin/contribution identity.
## Receipts And Failures
The external receipt is deterministic: `menu:<environment>:<step-idempotency-key>`. Duplicate `SUCCEEDED` delivery returns the stored receipt without touching menu data. Active target leases cannot be stolen; failed or expired attempts can be reclaimed through the shared target ledger.
All repository statements are fixed prepared statements owned by the platform. Plugins cannot supply SQL, arbitrary menu columns, target IDs, usernames, or raw error text.
## First Menu Delivery
`delivery.preview-menu@1.0.0` publishes the reviewed `menu:delivery-verification` contribution and an equivalent rollback document. Its H2 MySQL rehearsal first applies `delivery.preview-permission`, then proves Registry publication, Plan 1.2 resolution, menu apply, duplicate delivery, role visibility, reverse-order menu compensation, and final permission compensation.
The declared component is real. `generator/delivery/index.vue` calls a dedicated `GET /generator/plugin/delivery-verification` endpoint protected by `generator:delivery:verify`. The endpoint returns only execution environment, routing fingerprint, Executor routes, approval policy, installed Plugin count, and the non-secret JDBC target probe status.
## Probe And UI
When `menu-jdbc:v1` is selected by the default or route map, the manual JDBC probe requires the generic receipt ledger, both permission ownership tables, and both menu ownership tables. Missing menu tables return `MENU_LEDGER_UNAVAILABLE`; permission dependency failures remain distinguishable as `PERMISSION_LEDGER_UNAVAILABLE`.
The Plugin page shows permission and menu ownership readiness and treats all three JDBC adapters as sharing the isolated target configuration. The delivery verification page presents the frozen routing snapshot and target ledgers without exposing JDBC URL, credentials, driver exceptions, or SQLState.
## Compatibility And Scope
- Default routing remains `dry-run:v1`; real menu delivery requires an explicit `menu-admin-frontend` route.
- SQL-only and permission-only installations do not require menu ownership tables.
- The existing `sql-target` configuration prefix is retained for compatibility even though it now serves all JDBC adapters.
- The adapter creates one page menu under an existing parent, grants existing roles, and depends on an existing factory-owned permission. Each role must already own the full parent-menu ancestry and the required permission button. It does not create roles, shared parent grants, directories, frontend bundles, users, arbitrary menu trees, or external URLs.
- Compensation favors audit safety over forced cleanup and will not delete unmanaged grants or child menus.
- No external MySQL target was configured or changed in this stage; database integration tests use H2 in MySQL mode.
- Plaintext development credentials and TLS verification remain deferred by explicit user decision.