# Implementation Plan: Provider-Backed Action Preflight and Dispatch Gate Unification **Branch**: `216-provider-dispatch-gate` | **Date**: 2026-04-19 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/216-provider-dispatch-gate/spec.md` ## Summary Unify every covered operator-triggered provider-backed start behind the existing `ProviderOperationStartGate` so preventable provider blockers, same-operation dedupe, and cross-operation scope conflicts are resolved before queue admission instead of surfacing later as execution failures. The implementation extends the current gate and registry, adds one thin shared start-result presentation helper over the existing Ops UX stack, standardizes the public start wording as `queued`, `already running`, `scope busy`, and `blocked` over the canonical `accepted`, `deduped`, `scope busy`, and `blocked` result model, pins accepted work to an explicit `provider_connection_id`, keeps scheduled and system-run Monitoring language compatible without widening click-time UX scope, and migrates the first-slice tenant, provider-connection, restore, directory-sync, and onboarding start surfaces without adding new persistence or a second orchestration framework. ## Technical Context **Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 **Primary Dependencies**: Filament Resources/Pages/Actions, Livewire 4, Pest 4, `ProviderOperationStartGate`, `ProviderOperationRegistry`, `ProviderConnectionResolver`, `OperationRunService`, `ProviderNextStepsRegistry`, `ReasonPresenter`, `OperationUxPresenter`, `OperationRunLinks` **Storage**: PostgreSQL via existing `operation_runs`, `provider_connections`, `managed_tenant_onboarding_sessions`, `restore_runs`, and tenant-owned runtime records; no new tables planned **Testing**: Pest unit and focused feature tests for gate transitions, Filament action hosts, onboarding wizard flows, and Ops UX/run-detail alignment **Validation Lanes**: `fast-feedback`, `confidence` **Target Platform**: Laravel admin web app under Sail, rendered through Filament on Linux containers **Project Type**: Monorepo with one Laravel platform application in `apps/platform` plus docs/spec artifacts at repository root **Performance Goals**: Start preflight remains DB-only at click time, performs no Graph call before queue admission, adds no new remote latency source to the click-time path, and admits at most one accepted provider-backed run per protected scope **Constraints**: No new provider-start framework, no parallel queue-admission path, no new persistence, no operation-type naming rewrite, no inline remote work on start surfaces, no drift from RBAC 404/403 semantics, no drift from Ops-UX 3-surface feedback, and no hidden bypass of the gate on covered start surfaces **Scale/Scope**: First slice covers every current operator-triggered provider-backed start reachable from tenant-scoped surfaces, provider-connection surfaces, and onboarding: tenant verification, provider-connection check/inventory/compliance, restore execute, directory groups sync, role definitions sync, onboarding verification, and onboarding bootstrap ## Filament v5 Implementation Contract - **Livewire v4.0+ compliance**: Preserved. This feature stays within existing Filament v5 and Livewire v4 patterns and does not introduce any Livewire v3-era APIs. - **Provider registration location**: Unchanged. No panel/provider work is planned; existing panel providers remain registered in `bootstrap/providers.php`, not `bootstrap/app.php`. - **Global search coverage**: - `TenantResource` remains globally searchable and already has both view and edit pages. - `EntraGroupResource` remains tenant-scoped for global search and already has a view page. - `ProviderConnectionResource` keeps global search disabled (`$isGloballySearchable = false`). - `RestoreRunResource` is not being newly enabled for global search in this feature; its existing view page remains available if current search behavior references it. - **Destructive actions**: No new destructive actions are introduced. Restore execution remains a destructive-like flow that continues to rely on existing confirmation and authorization requirements; existing connection lifecycle actions remain `Action::make(...)->action(...)` based and keep `->requiresConfirmation()` where already required. - **Asset strategy**: No new panel or shared assets are planned. Deployment expectations remain unchanged, including `cd apps/platform && php artisan filament:assets` when registered Filament assets change. - **Testing plan**: Keep one supporting unit seam for the gate and shared presentation mapping, then prove the feature through focused Filament/Livewire feature coverage on tenant, provider-connection, onboarding, restore, directory-sync, and run-detail alignment. No browser lane or new heavy-governance family is planned. ## UI / Surface Guardrail Plan - **Guardrail scope**: Changed surfaces across existing tenant-scoped provider-backed starts, provider-connection start actions, restore execute, directory sync starts, onboarding provider steps, and canonical Monitoring run drill-in language - **Native vs custom classification summary**: Mixed shared-family change using native Filament actions, native notifications, and existing shared Ops UX helpers; no new custom shell or bespoke panel surface - **Shared-family relevance**: Shared provider action family across `TenantResource`, `TenantVerificationReport`, `ProviderConnectionResource`, `RestoreRunResource`, `ListEntraGroups`, `ManagedTenantOnboardingWizard`, and related run-detail explanation surfaces - **State layers in scope**: `page`, `detail`, `wizard-step`, and existing run-link drill-in; no new URL-query or shell ownership added - **Handling modes by drift class or surface**: Hard-stop for blocked preflight and protected-scope conflicts; review-mandatory for any remaining direct dispatch path on a covered surface - **Repository-signal treatment**: Review-mandatory because the feature changes a shared action-host family and canonical run-entry semantics - **Special surface test profiles**: `standard-native-filament`, `workflow / wizard`, `monitoring-state-page` - **Required tests or manual smoke**: `functional-core`, `state-contract`, `manual-smoke` - **Exception path and spread control**: One named exception boundary only: onboarding bootstrap currently batches multiple provider-backed operations and must be normalized to one accepted protected-scope run without introducing a new orchestration framework - **Active feature PR close-out entry**: `Guardrail` ## Constitution Check *GATE: Passed before Phase 0 research. Re-checked after Phase 1 design: still passed with one bounded helper addition and no new persisted truth.* | Gate | Status | Plan Notes | |------|--------|------------| | Inventory-first / read-write separation | PASS | The feature changes start admission and feedback only. Accepted work still executes asynchronously; restore keeps preview/confirmation/audit semantics and no new snapshot or runtime truth is introduced. | | Single Graph contract path / no inline remote work | PASS | Start surfaces remain authorize + preflight + enqueue only. Existing queued jobs continue to own Graph calls through the current provider abstractions; no render-time or click-time Graph call is added. | | RBAC, workspace isolation, tenant isolation | PASS | Covered starts remain inside existing tenant/workspace scopes. Non-members remain 404, members missing capability remain 403, and provider readiness messaging must not leak cross-tenant active runs or connection identity. | | Run observability / Ops-UX 3-surface feedback | PASS | Covered starts continue to create or reuse canonical `OperationRun` truth. Accepted starts keep toast-only intent feedback, progress stays in active widgets/run detail, and terminal DB notifications remain owned by `OperationRunService` for accepted work only. | | Proportionality / no premature abstraction / few layers | PASS | The plan extends the existing gate and registry and adds one thin presentation helper to absorb five-plus duplicated local result branches. No second gate, coordinator framework, or persisted summary layer is introduced. | | UI semantics / Filament-native action discipline | PASS | Existing action hosts remain in place. The feature standardizes start-result language without adding new navigation models, new shell surfaces, or page-local status frameworks. | | Test governance | PASS | Proof stays in unit plus focused feature lanes with explicit provider/tenant context opt-in. No new browser or heavy-governance family is required. | ## Test Governance Check - **Test purpose / classification by changed surface**: `Unit` for gate transition and presentation mapping seams; `Feature` for tenant/provider/onboarding/restore/directory start surfaces and accepted-run reason alignment; `Heavy-Governance`: none; `Browser`: none - **Affected validation lanes**: `fast-feedback`, `confidence` - **Why this lane mix is the narrowest sufficient proof**: The business truth is server-side preflight, authorization, dedupe/scope busy handling, queue admission, and consistent operator feedback on existing action hosts. That behavior is fully provable with direct feature tests and one small unit seam; browser coverage would add cost without proving unique behavior. - **Narrowest proving command(s)**: - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/Providers/ProviderOperationStartResultPresenterTest.php tests/Feature/ProviderConnections/ProviderDispatchGateStartSurfaceTest.php tests/Feature/Tenants/TenantProviderBackedActionStartTest.php tests/Feature/Workspaces/ManagedTenantOnboardingProviderStartTest.php tests/Feature/Restore/RestoreRunProviderStartTest.php tests/Feature/Directory/ProviderBackedDirectoryStartTest.php tests/Feature/Onboarding/OnboardingRbacSemanticsTest.php tests/Feature/Filament/RestoreRunUiEnforcementTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Operations/ProviderBackedRunReasonAlignmentTest.php tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Operations/OperationRunBlockedExecutionPresentationTest.php tests/Feature/OpsUx/Constitution/JobDbNotificationGuardTest.php tests/Feature/OpsUx/NoQueuedDbNotificationsTest.php tests/Feature/Guards/ProviderDispatchGateCoverageTest.php tests/Feature/Guards/TestLaneManifestTest.php` - **Fixture / helper / factory / seed / context cost risks**: Provider-backed start fixtures need tenant membership, explicit provider connections, active-run setup, and onboarding draft state; these remain explicit per test and must not become default helpers. - **Expensive defaults or shared helper growth introduced?**: No. Existing factories and helpers remain opt-in; no global provider/workspace bootstrap is planned. - **Heavy-family additions, promotions, or visibility changes**: None - **Surface-class relief / special coverage rule**: Standard native Filament relief for resource/page action hosts; onboarding uses a named wizard-step profile but still remains feature-testable without browser automation. - **Closing validation and reviewer handoff**: Re-run `pint`, then the focused test command above, then do one human smoke pass for blocked, deduped, scope busy, and queued outcomes across tenant, provider-connection, onboarding, and restore. Reviewers should confirm there is no remaining direct-dispatch bypass on a route-bounded covered surface, that scheduled or system-run Monitoring wording stays compatible with the same problem classes, and that no new custom completion notification path was introduced. - **Budget / baseline / trend follow-up**: None expected; document in feature if the new focused suite materially expands runtime beyond ordinary feature-local upkeep. - **Review-stop questions**: Did any coverage drift into browser/heavy lanes without a unique proving need? Did any helper start providing implicit tenant/provider context? Did any route-bounded covered start remain on local `ensureRun*/dispatch` logic? Did scheduled or system-run compatibility widen into click-time UX scope? Did any test assert presentation details that should stay in the shared presenter seam only? - **Escalation path**: `document-in-feature` - **Active feature PR close-out entry**: `Guardrail` - **Why no dedicated follow-up spec is needed**: This change is feature-local hardening around existing start surfaces. Only recurring operation-type normalization or structural lane-cost drift would justify a follow-up spec. ## Project Structure ### Documentation (this feature) ```text specs/216-provider-dispatch-gate/ ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── provider-dispatch-gate.logical.openapi.yaml └── tasks.md ``` ### Source Code (repository root) ```text apps/platform/ ├── app/ │ ├── Filament/ │ │ ├── Pages/Operations/TenantlessOperationRunViewer.php │ │ ├── Pages/Workspaces/ManagedTenantOnboardingWizard.php │ │ ├── Resources/EntraGroupResource/Pages/ListEntraGroups.php │ │ ├── Resources/OperationRunResource.php │ │ ├── Resources/ProviderConnectionResource.php │ │ ├── Resources/RestoreRunResource.php │ │ └── Resources/TenantResource.php │ ├── Notifications/ │ │ └── OperationRunCompleted.php │ ├── Services/ │ │ ├── Directory/RoleDefinitionsSyncService.php │ │ ├── Providers/ProviderConnectionResolver.php │ │ ├── Providers/ProviderOperationRegistry.php │ │ ├── Providers/ProviderOperationStartGate.php │ │ └── Verification/StartVerification.php │ └── Support/ │ ├── OperationRunLinks.php │ ├── OpsUx/OperationUxPresenter.php │ ├── OpsUx/ProviderOperationStartResultPresenter.php │ ├── ReasonTranslation/ │ └── Providers/ └── tests/ ├── Feature/ │ ├── Directory/ │ ├── Filament/ │ ├── Guards/ │ ├── Onboarding/ │ ├── Operations/ │ ├── OpsUx/ │ ├── ProviderConnections/ │ ├── Restore/ │ ├── Tenants/ │ └── Workspaces/ ├── Support/ └── Unit/Providers/ ``` **Structure Decision**: Single Laravel application inside the monorepo. All runtime work lands in `apps/platform`, while planning artifacts stay under `specs/216-provider-dispatch-gate`. ## Complexity Tracking No constitutional violation is planned. One bounded addition is tracked explicitly because it adds a thin presentation layer. | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | BLOAT-001 bounded presenter helper | Five-plus real start surfaces already duplicate blocked/deduped/scope busy/accepted notification branches; one shared helper is the narrowest way to keep one operator vocabulary without inventing a framework | Keeping page-local branches would fail FR-216-008/009, preserve inconsistent copy drift, and force every new covered surface to duplicate the same start contract again | ## Proportionality Review - **Current operator problem**: Operators can trigger provider-backed actions that look accepted at click time but only fail later because provider connection, consent, credential, or protected-scope prerequisites were missing. The same blocker also renders differently across tenant, provider-connection, onboarding, restore, and directory-sync surfaces. - **Existing structure is insufficient because**: The canonical gate already exists, but only a subset of surfaces use it and those surfaces still duplicate result rendering locally. Legacy `ensureRun*/dispatch` starts also resolve provider identity too late, which allows runtime default drift and inconsistent click-time feedback. - **Narrowest correct implementation**: Extend the existing `ProviderOperationRegistry` and `ProviderOperationStartGate`, add one thin start-result presentation helper over the current Ops UX primitives, and migrate the first-slice action hosts. Keep blocked/accepted truth in existing `OperationRun` records and do not add new persistence. - **Ownership cost created**: One small shared presentation helper, a broader but still focused feature test surface, and explicit registry entries for each newly covered operation type. - **Alternative intentionally rejected**: A new provider-start coordinator/orchestration framework or a second queue-admission path. Those would duplicate existing gate behavior, broaden the slice into architecture work, and violate FR-216-014. - **Release truth**: Current-release truth. The problem exists today on current operator-facing start surfaces. ## Phase 0 Research Summary - Reuse the current gate and registry; adoption breadth is the gap, not missing queue-admission infrastructure. - Resolve and pin `provider_connection_id` at dispatch time for every accepted covered start to prevent runtime default drift. - Preserve blocked preflight truth as canonical blocked start state, but never admit background work for blocked starts. - Add one shared presentation helper over `OperationUxPresenter`, `ReasonPresenter`, `ProviderNextStepsRegistry`, and `OperationRunLinks` rather than leaving local `Notification::make()` branches in place. - Keep the operator-facing start vocabulary explicit: the canonical outcomes remain `accepted`, `deduped`, `scope busy`, and `blocked`, while public wording stays `queued`, `already running`, `scope busy`, and `blocked`. - Keep the first implementation slice bounded to tenant, provider-connection, onboarding, restore, and directory start surfaces named by the spec; workspace-level baseline/evidence/review generators remain outside this route-bounded slice. - Do not combine dispatch-gate hardening with operation-type normalization; legacy write-time operation strings stay as-is in this feature. - Keep scheduled and system-triggered compatibility bounded to Monitoring-side reason reuse and initiator-aware notification behavior; no click-time UX is added for those runs. - Normalize onboarding bootstrap from multi-start batch admission to sequential protected-scope admission without adding a new orchestration framework. ## Phase 1 Design Summary - `data-model.md` documents the feature as a service-owned extension over existing `ProviderConnection`, `OperationRun`, and onboarding draft state plus new logical models for protected scope, preflight result, accepted run context, and shared presentation output. - `contracts/provider-dispatch-gate.logical.openapi.yaml` defines the internal action-start contract for tenant, provider-connection, restore, directory-sync, and onboarding action hosts plus canonical run-detail output. - `quickstart.md` defines the focused verification path for blocked, deduped, scope busy, and accepted results, including the onboarding bootstrap sequentialization case. ## Implementation Strategy 1. **Extend canonical gate coverage** - Expand `ProviderOperationRegistry` to include every first-slice covered operation type. - Keep the existing `ProviderOperationStartGate` as the single queue-admission path. - Ensure each migrated start resolves provider access before dispatch and passes the explicit `provider_connection_id` into both `OperationRun` context and job args. 2. **Centralize start-result presentation** - Add one shared helper that consumes `ProviderOperationStartResult` and emits the standardized operator feedback mapping: `accepted -> queued`, `deduped -> already running`, `scope busy -> scope busy`, and `blocked -> blocked`. - Reuse `OperationUxPresenter` for queued and already running toast semantics and `ReasonPresenter` plus next-step metadata for blocked and scope busy outcomes. - Keep domain verbs and target nouns local to each action host while unifying outcome vocabulary and run-link behavior. 3. **Migrate already-gated surfaces first** - Refactor `StartVerification`, `TenantResource`, `TenantVerificationReport`, `ProviderConnectionResource`, and the onboarding verification step to call the shared presentation helper instead of keeping local result-branching code. - Preserve current protection while removing duplicated operator copy paths. 4. **Migrate legacy direct-dispatch starts** - Replace local `ensureRun*/dispatch` logic on restore execute, directory groups sync, role definitions sync, and onboarding bootstrap with the canonical gate. - Keep destructive restore semantics intact: existing preview, warning, confirmation, and authorization remain unchanged; only queue admission and feedback unify. 5. **Normalize onboarding bootstrap sequencing** - Convert bootstrap admission from “start every selected provider-backed operation immediately” to “accept one protected-scope provider-backed operation at a time.” - Reuse existing onboarding draft state to retain pending bootstrap selections and run references instead of introducing an umbrella batch entity or new orchestration model. 6. **Align accepted-run diagnostics** - Ensure canonical run detail and terminal notification reason translation stay aligned with the shared start-result problem classes whenever they describe the same accepted operation. - Keep scheduled and system-triggered Monitoring reason reuse in the same public language family without adding click-time start UX or breaking initiator-only notification policy. - Keep blocked starts distinguishable from accepted execution failures in monitoring and audit surfaces. 7. **Backfill proof and regression guards** - Extend the gate unit suite for newly registered operation types and onboarding sequencing. - Add one route-bounded guard that inventories covered start hosts and fails if any first-slice action still bypasses `ProviderOperationStartGate` through local `ensureRun*/dispatch` logic. - Add focused feature coverage around each migrated action host and around cross-surface reason vocabulary alignment. ## Risks and Mitigations - **Onboarding bootstrap behavior change**: The current wizard can admit more than one provider-backed run for one connection. The mitigation is to normalize the flow to one accepted protected-scope run per submission and keep the rest as pending follow-up using existing draft state. - **Legacy operation-type aliases**: Directory sync operations currently use legacy write-time names. The mitigation is to keep those names stable in this feature and avoid widening the scope into operation taxonomy cleanup. - **Restore flow regression risk**: Restore is destructive-like and already confirmation-heavy. The mitigation is to change only the start path and feedback contract, not the preview/confirmation/authorization model. - **Hidden direct-dispatch bypasses**: Additional provider-backed starts may exist outside obvious action hosts. The mitigation is to treat any remaining first-slice `ensureRun*/dispatch` path as a review blocker and keep the route-bounded scope explicit. - **Over-testing via expensive fixtures**: Provider-backed starts need tenant, provider, and active-run context. The mitigation is to keep those fixtures opt-in and avoid a new shared “full provider world” helper. ## Post-Design Re-check Phase 0 and Phase 1 outputs resolve the earlier design unknowns without introducing new gates, new persisted truth, or a second UI semantics framework. The plan remains constitution-compliant, bounded to current-release operator pain, and ready for `/speckit.tasks`. ## Implementation Close-Out - **Final lane outcome**: Focused verification completed with `pint --dirty --format agent`, the documented quickstart suite, the broader migrated-surface regression suite, and the route-bounded guard/manifest close-out suite all passing during implementation. - **Route-bounded covered-surface audit**: `tests/Feature/Guards/ProviderDispatchGateCoverageTest.php` now guards the first-slice route-bounded provider-backed starts so tenant verification, provider-connection actions, restore execute, directory sync, role definitions sync, and onboarding bootstrap stay on canonical gate-owned entry points rather than regressing to local `ensureRun` / `dispatchOrFail` admission paths. - **Guardrail close-out**: Livewire v4.0+ compliance is preserved, provider registration remains in `bootstrap/providers.php`, no new global-search behavior was introduced, destructive restore semantics remain on existing confirmation/authorization paths, and no new Filament assets were added beyond the standing `php artisan filament:assets` deployment expectation when registered assets change. - **Test governance disposition**: `document-in-feature` remains the explicit disposition. The proof stays in unit plus focused feature lanes, with one new heavy-governance guard family (`provider-dispatch-gate-coverage`) recorded in the manifest instead of widening into browser or new heavy workflow suites.