TenantAtlas/specs/237-provider-boundary-hardening/plan.md
Ahmed Darrazi 079a7dcaf3
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 57s
feat: harden provider boundaries
2026-04-24 22:55:44 +02:00

254 lines
24 KiB
Markdown

# 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.