# Plugin Permission Target Executor Design ## Context P2-D8e2d can route each contribution to a frozen Executor and persist approval evidence, but only `sql-jdbc:v1` changes a real target. Permission contributions still use `dry-run:v1`; configuring the reserved permission route to any other code fails because no adapter is installed. P2-D8e2e introduces `permission-jdbc:v1` for RuoYi-compatible target databases. It consumes a narrow structured document, creates one Plugin-owned button permission, grants it to named roles, records target ownership, and safely compensates only the resources it owns. ## Target And Isolation The adapter reuses the existing isolated JDBC target configuration and connection factory: - `factory.plugin-execution.sql-target` supplies environment, catalog, connection, and lease settings; - `factory_plugin_target_receipt` remains the durable per-step lease and idempotency ledger; - the target must be separate from the Factory control database; - `sys_menu`, `sys_role`, and `sys_role_menu` must be RuoYi-compatible; - permission ownership tables must be installed by the updated target bootstrap script. The adapter never executes SQL supplied by a Plugin. All statements are fixed prepared statements owned by the platform. ## Permission Document Each `PERMISSION + backend` contribution contains exactly one JSON document: ```json { "schemaVersion": "1.0", "permissionCode": "generator:delivery:verify", "displayName": "交付验证", "parentPermissionCode": "generator:plugin:list", "roleKeys": ["common"] } ``` Validation is strict: - only the five published fields are accepted; - `schemaVersion` must be `1.0`; - `permissionCode` must equal the immutable contribution key and use a three-part lowercase permission identifier; - wildcard permission identifiers are rejected; - `parentPermissionCode` must identify a different existing active menu; - display name is non-blank and at most 50 characters; - one to 20 unique lowercase role keys are allowed; - the document is bounded to 64 KiB. Role keys are sorted before computing a semantic document fingerprint. Apply and rollback resources may differ in whitespace or role ordering, but must describe the same semantic permission identity. ## Apply Semantics After claiming the generic target receipt lease, the adapter runs one database transaction: 1. Resolve exactly one active parent menu by `parentPermissionCode`. 2. Resolve exactly one active `sys_role` row for every role key. 3. Lock the ownership row for `permissionCode`. 4. Reject an existing unowned `sys_menu.perms` collision. 5. Create a button (`menu_type = 'F'`) with a generated target-local `menu_id`. 6. Persist the Plugin owner, contribution key, semantic fingerprint, parent identity, generated menu ID, and `ACTIVE` status. 7. Insert only the declared `sys_role_menu` grants and matching ownership rows. An interrupted attempt can be reclaimed through the existing lease. If the ownership row is already `ACTIVE`, every immutable identity, menu field, owned role grant, and role ID must still match before the attempt can complete. Drift is rejected rather than overwritten. ## Compensation Semantics Compensation locks and validates the ownership row before deletion. It refuses to continue when: - the permission is not owned by the same Plugin and contribution key; - the semantic document fingerprint differs; - the target menu or an owned role grant has drifted; - another role was granted the Plugin-owned menu outside the ownership ledger; - the button unexpectedly has child menus. When validation succeeds, one transaction deletes the owned role grants and button menu, removes active grant rows, and marks the permission ownership row `REMOVED`. The tombstone makes an interrupted compensation retry idempotent and preserves audit identity. A later apply by the same Plugin/contribution key may reactivate the permission with a new target menu ID after the prior version has been compensated. ## Receipts And Failures The external receipt is deterministic: `permission::`. Duplicate `SUCCEEDED` delivery returns the stored receipt without touching target authorization rows. Active leases cannot be stolen; failed or expired attempts can be reclaimed. All target errors are sanitized. JDBC URLs, credentials, SQL details, role IDs, and driver exceptions are not returned through control-side execution history. ## First Delivery Plugin `delivery.preview-permission@1.0.0` publishes the reviewed `generator:delivery:verify` permission document and its compensation resource. The rehearsal target supplies an existing `generator:plugin:list` parent and `common` role, then proves publish-plan resolution, apply, duplicate delivery, role authorization, compensation, and duplicate compensation against H2 in MySQL mode. ## Probe And UI When `permission-jdbc:v1` is selected by the default or route map, the existing manual JDBC target probe additionally checks both permission ownership tables. A missing permission schema has its own non-secret state and blocks a READY result. The Plugin page recognizes that state and treats both SQL and permission JDBC adapters as requiring the same isolated target readiness. ## Compatibility And Scope - Default routing stays `dry-run:v1`. - SQL-only installations do not require permission ownership tables unless `permission-jdbc:v1` is configured. - Existing SQL receipts and SQL Executor behavior are unchanged. - This stage does not create directory/menu pages, backend endpoints, users, roles, or arbitrary permission hierarchies. - It does not adopt pre-existing permission rows or delete unmanaged role grants. - Multi-person approval, external tickets, permission cache invalidation across a running target cluster, and the real admin-menu adapter remain later work. - Plaintext development credentials and TLS verification remain deferred by explicit user decision.