# Implementation Plan: Provider Boundary Hardening **Branch**: `237-provider-boundary-hardening` | **Date**: 2026-04-24 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/237-provider-boundary-hardening/spec.md` **Note**: This plan keeps the slice intentionally narrow. It classifies the first high-risk shared provider seams, removes Graph-shaped request building from shared identity resolution, makes provider binding explicit in the shared operation registry path, and adds focused guardrails without introducing a second-provider runtime, new persistence, or new operator-facing surfaces. ## Summary Add a config-seeded provider-boundary catalog plus a small `App\Support\Providers\Boundary` helper layer to classify the first hot seams as `provider_owned` or `platform_core`, with documented current-release exception metadata where needed. The authoritative first-slice seam inventory is locked to `provider.gateway_runtime`, `provider.identity_resolution`, `provider.connection_resolution`, `provider.operation_registry`, and `provider.operation_start_gate`. The implementation will harden two concrete hotspots: first, move Graph request-option shaping out of `ProviderIdentityResolution` and keep it inside provider-owned seams such as `ProviderGateway` and `MicrosoftGraphOptionsResolver`; second, split `ProviderOperationRegistry` into platform-core operation metadata plus explicit provider binding metadata so `ProviderOperationStartGate` no longer treats `microsoft` as silent platform-default truth. Existing Microsoft-backed flows stay intact, `entra_tenant_id` and platform app identity remain documented current-release exceptions for the follow-up identity-neutrality slice, and the boundary is enforced through focused unit plus feature guard tests. ## Technical Context **Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 **Primary Dependencies**: existing provider seams under `App\Services\Providers` and `App\Services\Graph`, especially `ProviderGateway`, `ProviderIdentityResolver`, `ProviderIdentityResolution`, `PlatformProviderIdentityResolver`, `ProviderConnectionResolver`, `ProviderConnectionResolution`, `MicrosoftGraphOptionsResolver`, `ProviderOperationRegistry`, `ProviderOperationStartGate`, `GraphClientInterface`, Pest v4 **Storage**: Existing PostgreSQL tables such as `provider_connections` and `operation_runs`; one new in-repo config catalog for provider-boundary ownership; no new database tables **Testing**: Pest v4 unit and focused feature tests through Laravel Sail **Validation Lanes**: `fast-feedback`, `confidence` **Target Platform**: Laravel admin web application running in Sail on the existing `/admin`, `/admin/t/{tenant}`, and provider-backed operation surfaces **Project Type**: Monorepo with one Laravel runtime in `apps/platform` and spec artifacts at repository root **Performance Goals**: Keep boundary evaluation deterministic and in-process, add no outbound call before existing provider-owned execution seams, and preserve current provider-backed runtime performance on supported Microsoft flows **Constraints**: No new provider runtime, no broad provider marketplace abstraction, no schema or route redesign, no operation-type renaming, no new UI surface, no new Graph contract path, and no silent Microsoft fallback on touched shared seams **Scale/Scope**: One config-backed boundary catalog, one small boundary support namespace, one shared identity-resolution cleanup, one shared operation-registry cleanup, and focused unit plus feature guard coverage ## Filament v5 Implementation Contract - **Livewire v4.0+ compliance**: Preserved. This slice changes shared services, value objects, and guardrails only and introduces no legacy Livewire patterns. - **Provider registration location**: Unchanged. Panel providers remain registered in `apps/platform/bootstrap/providers.php`. - **Global search coverage**: No new Filament Resource or Page is added, and no existing global-search posture changes in this slice. Provider connection surfaces remain on their current search posture. - **Destructive actions**: No destructive action is added or changed. This slice does not introduce new Filament actions. - **Asset strategy**: No new assets are planned. Deployment expectations remain unchanged, including `cd apps/platform && php artisan filament:assets` only when later UI work introduces registered assets. - **Testing plan**: Prove the slice with focused Pest unit coverage for seam classification and registry behavior plus focused feature coverage for current Microsoft runtime preservation, unsupported-path behavior, and boundary guardrails. ## UI / Surface Guardrail Plan - **Guardrail scope**: workflow-only guardrail change - **Native vs custom classification summary**: `N/A` - **Shared-family relevance**: provider-backed execution seams, provider connection runtime semantics, shared architecture guards - **State layers in scope**: none - **Handling modes by drift class or surface**: `review-mandatory` - **Repository-signal treatment**: `review-mandatory` - **Special surface test profiles**: `N/A` - **Required tests or manual smoke**: `functional-core`, `state-contract` - **Exception path and spread control**: one named current-release exception boundary for Microsoft-specific target-scope and platform app identity semantics that remain until the follow-up identity-neutrality spec - **Active feature PR close-out entry**: `Guardrail` ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes - **Systems touched**: provider gateway/runtime access, provider identity resolution, provider connection validation, provider-backed operation registry and start gate, provider-owned reason and next-step semantics, and adjacent feature guard patterns - **Shared abstractions reused**: existing provider services, existing `GraphClientInterface` contract, existing `ProviderOperationStartGate`, existing feature-guard patterns under `tests/Feature/Guards` such as `NoLegacyTenantGraphOptionsTest.php` and `NoLegacyTenantProviderFallbackTest.php`, and existing provider unit suites - **New abstraction introduced? why?**: yes. A small boundary catalog and boundary descriptor layer are required because prose and generic class names alone are not machine-checkable and have not prevented drift. - **Why the existing abstraction was sufficient or insufficient**: existing provider seams are sufficient as extension points, but insufficiently explicit about ownership. `ProviderIdentityResolution::graphOptions()` and `ProviderOperationRegistry` currently mix provider-specific semantics into shared paths. - **Bounded deviation / spread control**: the only allowed retained deviation is the documented Microsoft-first identity/target-scope exception on existing provider connection data until the follow-up identity-neutrality slice lands ## Provider Boundary & Portability Fit - **Shared provider/platform boundary touched?**: yes - **Provider-owned seams**: `GraphClientInterface` implementations, `ProviderGateway`, `MicrosoftGraphOptionsResolver`, and Intune-specific service calls that intentionally execute Microsoft behavior - **Platform-core seams**: provider-boundary catalog, `ProviderIdentityResolver`, `ProviderIdentityResolution`, `ProviderConnectionResolver`, `ProviderConnectionResolution`, `ProviderOperationRegistry` core operation definition path, `ProviderOperationStartGate` shared orchestration decisions - **Authoritative first-slice seam inventory**: - `provider.gateway_runtime` -> `ProviderGateway.php`, `MicrosoftGraphOptionsResolver.php` - `provider.identity_resolution` -> `ProviderIdentityResolution.php`, `ProviderIdentityResolver.php`, `PlatformProviderIdentityResolver.php` - `provider.connection_resolution` -> `ProviderConnectionResolver.php`, `ProviderConnectionResolution.php` - `provider.operation_registry` -> `ProviderOperationRegistry.php` - `provider.operation_start_gate` -> `ProviderOperationStartGate.php` - **Neutral platform terms / contracts preserved**: provider, provider connection, target scope, operation type, operation module, required capability, provider binding, unsupported provider behavior - **Retained provider-specific semantics and why**: `entra_tenant_id`, platform app credential config, redirect callback details, and Microsoft Graph request-option keys remain current-release Microsoft semantics because they are still needed for the only shipped provider runtime today - **Bounded extraction or follow-up path**: `follow-up-spec` for Provider Identity & Target Scope Neutrality; this feature documents and bounds the remaining identity-shaped hotspot instead of solving schema and UI neutrality here ## Constitution Check *GATE: Passed before Phase 0 research. Re-check after Phase 1 design: still passed with one config-backed seam catalog, one bounded runtime extraction, and no new persistence or operator surface.* | Gate | Status | Plan Notes | |------|--------|------------| | Inventory-first / read-write separation | PASS | The slice hardens contracts and runtime boundaries only. No new write path, preview flow, or operator mutation surface is introduced. | | Single Graph contract path / no inline remote work | PASS | Existing Graph calls remain behind `GraphClientInterface`. The slice only relocates Graph option shaping to provider-owned seams and adds no new contract bypass. | | RBAC, workspace isolation, tenant isolation | PASS | No new route, capability, or authorization plane is introduced. Existing provider-backed workflows keep their current tenant and workspace guards. | | Run observability / Ops-UX lifecycle | PASS | The feature may touch `ProviderOperationStartGate`, but it does not create a new `OperationRun` type or change start-surface UX semantics. Existing service-owned run lifecycle rules remain intact. | | Shared pattern first | PASS | The implementation reuses existing provider services and the existing guard-test pattern instead of creating a parallel portability framework. | | Proportionality / no premature abstraction | PASS | One boundary catalog plus small descriptors are the narrowest machine-checkable source of truth for multiple real seams. No plugin system, provider marketplace, or second runtime is introduced. | | Persisted truth / behavioral state | PASS | No new table or persisted lifecycle is added. One or two new provider-boundary reason codes may be introduced only if explicit unsupported-path behavior needs stable runtime semantics. | | Provider boundary | PASS | The plan explicitly separates provider-owned seams from platform-core seams and records one bounded Microsoft-first exception path. | | Filament v5 / Livewire v4 contract | PASS | No new Filament surface, action, or global-search behavior is introduced. Provider registration remains in `bootstrap/providers.php`. | | Test governance | PASS | Coverage stays in focused unit plus feature lanes with no browser or heavy-governance expansion. | ## Test Governance Check - **Test purpose / classification by changed surface**: `Unit` for seam classification, registry split semantics, and retained exception behavior; `Feature` for current Microsoft-backed runtime preservation, unsupported-path behavior, and boundary guardrails on touched shared seams - **Affected validation lanes**: `fast-feedback`, `confidence` - **Why this lane mix is the narrowest sufficient proof**: The business risk is semantic drift inside shared code, not browser interaction. Unit tests prove classification and neutral-contract rules; feature tests prove current Microsoft-backed flows remain intact and unsupported shared-boundary cases fail explicitly. - **Narrowest proving command(s)**: - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - **Fixture / helper / factory / seed / context cost risks**: Minimal. Reuse existing `ProviderConnection` and tenant factories plus current provider unit tests. Do not introduce a new default provider world helper. - **Expensive defaults or shared helper growth introduced?**: No. The boundary catalog stays config-backed and test fixtures remain opt-in. - **Heavy-family additions, promotions, or visibility changes**: none - **Surface-class relief / special coverage rule**: `N/A` - **Closing validation and reviewer handoff**: Reviewers should verify that `ProviderIdentityResolution` no longer shapes Graph request options, that shared operation metadata no longer treats `microsoft` as silent default truth, that the remaining Microsoft-specific identity fields are documented as exceptions, and that current Microsoft-backed starts still work through the hardened seams. - **Budget / baseline / trend follow-up**: none expected - **Review-stop questions**: Did any platform-core seam retain Graph request shaping? Did `ProviderOperationRegistry` still expose provider binding as primary platform truth? Did the slice widen into schema/UI neutrality or operation-type renaming? Did any new test helper make provider context implicit by default? - **Escalation path**: `document-in-feature` - **Active feature PR close-out entry**: `Guardrail` - **Why no dedicated follow-up spec is needed**: The remaining identity-schema and UI neutrality work already has a named next candidate. This feature contains only the first bounded hardening pass. ## Project Structure ### Documentation (this feature) ```text specs/237-provider-boundary-hardening/ ├── spec.md ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── provider-boundary-hardening.logical.openapi.yaml └── tasks.md ``` ### Source Code (repository root) ```text apps/platform/ ├── app/ │ ├── Models/ │ │ └── ProviderConnection.php │ ├── Services/ │ │ ├── Graph/ │ │ │ ├── GraphClientInterface.php │ │ │ └── MicrosoftGraphClient.php │ │ └── Providers/ │ │ ├── MicrosoftGraphOptionsResolver.php │ │ ├── PlatformProviderIdentityResolver.php │ │ ├── ProviderConnectionResolution.php │ │ ├── ProviderConnectionResolver.php │ │ ├── ProviderGateway.php │ │ ├── ProviderIdentityResolution.php │ │ ├── ProviderIdentityResolver.php │ │ ├── ProviderOperationRegistry.php │ │ └── ProviderOperationStartGate.php │ └── Support/ │ └── Providers/ │ └── Boundary/ ├── config/ │ └── provider_boundaries.php └── tests/ ├── Feature/ │ ├── Providers/ │ └── Guards/ └── Unit/ └── Providers/ ``` **Structure Decision**: Keep the entire slice inside the existing Laravel runtime in `apps/platform`. The only new top-level code shape is a small `Support/Providers/Boundary` namespace plus a config-backed seam catalog. Runtime changes stay inside the existing provider services and the shared provider operation registry path. ## Complexity Tracking No constitutional violation is planned. One bounded complexity addition is tracked because the feature introduces a new source of truth for seam ownership. | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | BLOAT-001 bounded boundary catalog | Multiple real shared seams now need one explicit, testable ownership source of truth so provider leakage stops depending on reviewer memory alone | Comments, prose-only notes, or local assertions would not be machine-checkable and would let each new seam drift independently | ## Proportionality Review - **Current operator problem**: Shared provider-backed platform code can still silently become more Microsoft-shaped, which raises the cost and risk of future governance work even when current operator behavior still appears to work. - **Existing structure is insufficient because**: generic class names and partial provider abstractions do not stop Graph request shaping, provider binding defaults, and Microsoft-specific semantics from leaking into shared resolution or orchestration paths. - **Narrowest correct implementation**: add one config-backed seam catalog, extract Graph option shaping out of shared identity resolution, and separate provider binding from shared operation metadata at the existing registry/start-gate seam. - **Ownership cost created**: maintain the seam catalog, preserve a small set of boundary guard tests, and keep the one documented Microsoft-first exception path explicit until the follow-up identity-neutrality work lands. - **Alternative intentionally rejected**: a broad multi-provider framework or connector platform. It would import speculative runtime machinery before there is a second real provider case. - **Release truth**: current-release truth with deliberate anti-drift preparation for the next provider-boundary follow-through specs ## Phase 0 Research Summary - The first boundary hardening slice should use a small config-backed seam catalog, not a generic provider-plugin framework. - `ProviderIdentityResolution::graphOptions()` is a concrete provider-leak hotspot because a shared resolution object currently shapes Microsoft Graph request options directly. - `ProviderOperationRegistry` is a second concrete hotspot because shared operation definitions currently expose `microsoft` as if it were platform-default truth. - Existing `ProviderGateway`, `MicrosoftGraphOptionsResolver`, and Intune-specific services are acceptable provider-owned seams for current Microsoft behavior. - `entra_tenant_id`, platform app identity config, and callback/redirect details should remain explicit current-release exceptions here and be cleaned up in the follow-up identity-neutrality slice. - Focused unit plus feature guard tests are sufficient; browser or heavy-governance coverage would add cost without proving unique behavior. ## Phase 1 Design Summary - `research.md` records the boundary decisions that keep the slice narrow and explicit. - `data-model.md` defines the seam ownership catalog, operation definition vs provider binding split, and boundary guard result shape. - `contracts/provider-boundary-hardening.logical.openapi.yaml` defines the logical internal contract for listing seam ownership and evaluating boundary changes. - `quickstart.md` records the narrow validation order and the intended code areas. - `tasks.md` will sequence the work from seam-catalog foundation through shared identity and registry hardening to final guard coverage. ## Phase 1 — Agent Context Update Run after artifact generation: - `.specify/scripts/bash/update-agent-context.sh copilot` ## Implementation Strategy ### Phase A — Add the seam ownership catalog **Goal**: Make the authoritative first-slice seams explicitly classifiable and testable. | Step | File | Change | |------|------|--------| | A.1 | `apps/platform/config/provider_boundaries.php` | Add the bounded seam catalog for `provider.gateway_runtime`, `provider.identity_resolution`, `provider.connection_resolution`, `provider.operation_registry`, and `provider.operation_start_gate`, classifying each as `provider_owned` or `platform_core` and recording retained-provider-semantic notes as exception metadata where needed. | | A.2 | `apps/platform/app/Support/Providers/Boundary/ProviderBoundaryOwner.php`, `ProviderBoundarySeam.php`, and `ProviderBoundaryCatalog.php` | Model seam ownership, allowed exceptions, and deterministic lookup for tests and runtime guard checks. | | A.3 | `apps/platform/tests/Unit/Providers/ProviderBoundaryClassificationTest.php` | Prove the catalog contains the intended first-slice seams and only the allowed ownership classifications. | ### Phase B — Move Graph request shaping behind provider-owned seams **Goal**: Stop shared identity resolution from emitting Microsoft Graph-shaped runtime options directly. | Step | File | Change | |------|------|--------| | B.1 | `apps/platform/app/Services/Providers/ProviderIdentityResolution.php` | Remove Graph request-option shaping from the shared resolution object and expose only the neutral runtime data the provider-owned seam needs. | | B.2 | `apps/platform/app/Services/Providers/ProviderGateway.php` and `MicrosoftGraphOptionsResolver.php` | Own Graph option assembly inside provider-owned seams and reuse the shared resolution data without reintroducing platform-core Graph leakage. | | B.3 | `apps/platform/app/Services/Providers/ProviderIdentityResolver.php`, `PlatformProviderIdentityResolver.php`, and `ProviderConnectionResolver.php` | Keep current Microsoft-first identity semantics working while marking the remaining target-scope and platform-app details as explicit current-release exceptions. | ### Phase C — Split shared operation metadata from provider binding **Goal**: Keep shared orchestration metadata platform-core while making provider binding explicit and bounded. | Step | File | Change | |------|------|--------| | C.1 | `apps/platform/app/Services/Providers/ProviderOperationRegistry.php` | Separate platform-core operation definition fields from provider-binding fields so the shared definition does not treat `microsoft` as silent default truth. | | C.2 | `apps/platform/app/Services/Providers/ProviderOperationStartGate.php` | Consume the explicit provider binding, preserve current Microsoft-backed start behavior, and return explicit unsupported behavior when a touched shared seam has no provider-owned binding. | | C.3 | `apps/platform/app/Support/Providers/ProviderReasonCodes.php` and adjacent translation helpers if needed | Add one narrow provider-boundary reason code only if explicit unsupported shared-boundary behavior needs stable runtime semantics. | ### Phase D — Add guardrails and preserve runtime behavior **Goal**: Keep the boundary enforceable without widening the slice. | Step | File | Change | |------|------|--------| | D.1 | `apps/platform/tests/Unit/Providers/ProviderBoundaryGuardrailTest.php` | Prove platform-core seam rules, allowed exceptions, and registry split behavior are deterministic. | | D.2 | `apps/platform/tests/Feature/Providers/ProviderBoundaryHardeningTest.php` | Prove a current Microsoft-backed workflow still succeeds through the hardened seams. | | D.3 | `apps/platform/tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php` | Prove the touched shared seam fails explicitly rather than inheriting Microsoft default behavior when binding or ownership is absent. | | D.4 | `specs/237-provider-boundary-hardening/quickstart.md` and `tasks.md` | Keep the validation order, exception boundary, and no-second-provider-runtime guardrail explicit. | ## Risks and Mitigations - **Identity scope creep**: Boundary hardening could drift into full provider identity neutrality. Mitigation: keep `entra_tenant_id` and platform app identity as explicit current-release exceptions and defer schema/UI neutrality to the next spec. - **Operation-type scope creep**: Registry cleanup could become operation-type canonicalization work. Mitigation: keep operation type values unchanged and limit this slice to ownership separation, not naming reform. - **Guardrail overreach**: A broad filesystem scan could flag legitimate provider-owned Microsoft services. Mitigation: make the seam catalog the allowlist source of truth and keep the guard coverage focused on touched shared seams. - **Runtime regression**: Moving Graph option shaping can break current Microsoft-backed flows. Mitigation: preserve and extend focused provider unit and feature coverage around the hardened gateway and registry paths. ## Post-Design Re-check The feature remains constitution-compliant, Filament v5 and Livewire v4 compliant, and narrow. It introduces no new persistence, no new operator-facing page, no new provider runtime, and no operation-type renaming. The plan, research, data model, quickstart, contract, and later tasks align on one explicit seam catalog, one provider-owned Graph shaping boundary, one shared registry hardening step, and one bounded Microsoft-first exception path.