# Implementation Report: Exchange Credential and Permission Evidence Support ## Result - **Gate result**: PASS WITH CONDITIONS. - **Active spec**: `specs/433-exchange-credential-permission-evidence-support/`. - **Branch**: `433-exchange-credential-permission-evidence-support`. - **HEAD at implementation start**: `f4e34212 feat: add Exchange PowerShell production runner gate (#499)`. - **Initial dirty state**: only untracked active Spec 433 package. - **Final dirty state**: application/config/test changes plus the active Spec 433 package and this report. - **Condition**: browser Product Surface sanity found an existing broader provider-readiness guidance card can still show a generic provider credential blocker next to the Exchange-specific capability result. The changed Exchange capability slots themselves were canonical and redacted. No UI code was changed because this spec forbids new UI/surface work. ## Activated Skills and Gates - `spec-kit-implementation-loop`: active implementation workflow. - `pest-testing`: Pest 4 focused unit/feature coverage. - Browser skills: `browser:control-in-app-browser` and repo `browsertest` for focused read-only browser proof. - TenantPilot repo gates: spec readiness, workspace-scope safety, RBAC/action safety, provider freshness semantics, OperationRun truth, evidence-anchor contract, Product Surface gate, Filament/Livewire v5 change loop. ## Implementation Summary - Added `ExchangePowerShellInvocationReadinessEvaluator` as the shared readiness truth for `exchange_powershell_invoke` capability evaluation and the production runner gate. - Kept the no-migration path. Existing `ProviderCredential` fields and `ManagedEnvironmentPermission.details` are sufficient when permission details explicitly match workspace, managed environment, provider, and provider connection. - Tightened certificate readiness so certificate references require both rotation and expiry metadata. - Extended `ExchangePowerShellPermissionEvidenceEvaluator` with supported-provider checks, status normalization, configurable currentness, safe source metadata, scoped provider-connection validation, and sanitized blocker states. - Post-review hardening requires trusted permission evidence provenance: `source=provider_verification`, canonical, calendar-valid ISO/RFC3339 non-future `observed_at`/`verified_at`, `verified_at >= observed_at`, evaluator identity/version metadata, and current `last_checked_at` plus current observed/verified timestamps. Missing, malformed, relative, untrusted, or time-inconsistent metadata now fails closed as unvalidated evidence; stale row or provenance timestamps fail closed as stale evidence. - Post-review scope hardening requires `workspace_id`, `managed_environment_id`, and `provider_connection_id` evidence metadata to be canonical positive integer values before any scope comparison. Numeric-prefix, decimal, scientific-notation, whitespace-padded, missing, or otherwise malformed scope IDs now fail closed as unvalidated evidence instead of passing through PHP integer coercion. - Updated `ProviderCapabilityEvaluator` so `exchange_powershell_invoke` is Supported only when combined credential and permission readiness passes. - Post-review hardening makes the shared Exchange readiness evaluator the single capability truth for `exchange_powershell_invoke`; the capability no longer applies a separate generic provider-readiness freshness precheck that can diverge from the Exchange evidence currentness config. - Updated `ExchangePowerShellProductionRunner` to consume combined readiness before runtime checks or process execution. - Added safe `readiness_state` and `readiness_blocker` propagation into OperationRun runner-result context. - Added env/config defaults for `TENANTPILOT_EXCHANGE_POWERSHELL_PERMISSION_EVIDENCE_CURRENTNESS_DAYS=30`. ## Data Model Decision | Area | Decision | Proof | | --- | --- | --- | | Provider credential | No migration | `provider_connection_id`, `credential_kind`, `source`, `last_rotated_at`, and `expires_at` cover the observable readiness states for this slice. | | Permission evidence | No migration | `ManagedEnvironmentPermission` remains one row per environment + permission key; `details` must match workspace, environment, provider, and provider connection or fail closed. | | Provider connection scope | Existing model | Readiness requires matching `workspace_id` and `managed_environment_id`; wrong connection details block. | | Secret material | No new storage | Existing encrypted payload remains hidden and is never returned in readiness/OperationRun context. | | Migration lane | N/A | No schema file added; no PostgreSQL schema lane required. | ## Credential Matrix | Credential state | Result | | --- | --- | | Missing reference | Blocks as `credential_blocked_missing_reference`. | | Client secret | Blocks as `credential_blocked_secret_based_app`. | | Certificate missing rotation or expiry metadata | Blocks as `credential_blocked_certificate_missing`. | | Expired certificate | Blocks as `credential_blocked_certificate_expired`. | | Certificate unsupported by config | Blocks as `credential_blocked_certificate_unsupported`. | | Certificate explicitly supported for readiness | Passes as `certificate_supported`. | | Federated | Blocks unless future config/support is added. | | Managed identity string | Blocks as unsupported/unknown. | | Unknown kind | Blocks fail-closed. | ## Permission Matrix | Permission evidence state | Result | | --- | --- | | Missing row | Blocks as `permission_evidence_blocked_missing`. | | Missing/absent/revoked status | Blocks as `permission_evidence_blocked_absent`. | | Unknown/error status | Blocks as `permission_evidence_blocked_unknown`. | | Granted without trusted source, observed/verified timestamps, evaluator identity, or evaluator version | Blocks as unvalidated. | | Granted with malformed, relative, future, or time-inconsistent provenance metadata | Blocks as unvalidated. | | Granted with malformed scope identifiers | Blocks as unvalidated before scope matching. | | Stale row, observed timestamp, or verified timestamp beyond configured currentness | Blocks as `permission_evidence_blocked_stale`. | | Wrong workspace/environment/provider/provider connection | Blocks with scoped failure code. | | Current scoped `Exchange.ManageAsApp` | Passes as `verified`. | ## Capability and Runner Proof - `exchange_powershell_invoke` uses the combined readiness evaluator and no longer relies on admin consent, generic provider freshness, or generic stored permission rows alone. - Missing credential maps to Missing; stale/unknown/unvalidated permission evidence maps to Unknown; wrong-scope or unsupported readiness maps to Blocked. - The production runner evaluates combined readiness before runtime readiness, command building, locks, or process execution. - Successful runner context persists only safe state: runner mode, execution enabled, readiness state, runtime state, credential state, permission evidence state, command builder/output/concurrency state, counts, and duration. ## No-Promotion Proof - No `TenantConfigurationResource` or `TenantConfigurationResourceEvidence` rows are created. - No raw or normalized Exchange payload, compare/render/certification/restore/customer-ready state, report output, Review Pack output, PDF output, route, Filament page/resource/widget/action, Livewire component, navigation, asset, global search change, job, schedule, listener, console trigger, Exchange-specific table, legacy shim, fallback reader, dual write, or `tenant_id` ownership path was introduced. - Completed Specs 429-432 were read-only context and were not rewritten. ## Product Surface and Filament Close-Out - **No-legacy posture**: canonical addition only; no aliases, fallback readers, hidden routes, duplicate UI, or historical compatibility path. - **Product Surface Impact**: existing provider capability/readiness surfaces can render changed evaluator output; no new surface budget introduced. - **UI Surface Impact**: no UI files changed; existing provider connection and required-permissions surfaces render the changed status data. - **Browser proof**: PASS WITH CONDITION. See browser section below. - **Human Product Sanity**: PASS WITH CONDITION. Exchange capability slots are canonical/redacted; existing broader readiness card can still show a generic credential blocker beside Exchange-specific Supported. - **Visible complexity outcome**: neutral; no new visible components, only more truthful existing status values. - **Livewire v4 compliance**: unchanged repo baseline; no Livewire runtime code added. - **Provider registration location**: no Filament panel provider registration changed; Laravel panel providers remain in `apps/platform/bootstrap/providers.php`. - **Global search**: no Filament resources or global search behavior changed. - **Destructive/high-impact actions**: no UI action added; runner remains behind existing Spec 432 gates and provider-run authorization. - **Asset strategy**: no assets; `filament:assets` not required for this slice. - **Deployment impact**: no migration, queue, schedule, storage, asset, or build impact. New env example key: `TENANTPILOT_EXCHANGE_POWERSHELL_PERMISSION_EVIDENCE_CURRENTNESS_DAYS=30`. ## Browser Proof - Sail was running and migrations were current. A local smoke fixture was seeded with: - ready environment + certificate credential + scoped current `Exchange.ManageAsApp` evidence + all normal required permission rows; - blocked environment + client-secret credential + stale permission evidence. - A temporary ignored local `.env` override `TENANTPILOT_EXCHANGE_POWERSHELL_SUPPORTED_REFERENCE_KINDS=certificate` was used only during browser proof and removed afterward; config cache was cleared after removal. - Provider connection view, ready fixture: - `Exchange PowerShell invocation: Supported` rendered in existing capability detail. - No raw grant, access token, private key, certificate password, or client secret sentinel rendered. - No browser console errors from the tab. - Provider connection view, blocked fixture: - `Exchange PowerShell invocation: Unknown` rendered for the blocked client-secret/stale-evidence fixture. - No raw token/client-secret sentinel rendered. - No browser console errors from the tab. - Required-permissions view, ready fixture: - `Provider capabilities` section rendered `Exchange PowerShell invocation` as Supported with `0 missing, 0 need review`. - No raw grant, access token, private key, certificate password, or client secret sentinel rendered. - No browser console errors from the tab. - Required-permissions view, blocked fixture: - Required-permissions/provider capability surface stayed non-supported and showed missing/stale guidance rather than ready. - No raw token/client-secret sentinel rendered. - No browser console errors from the tab. - Laravel Boost browser log contained older unrelated June operation-page errors, not Spec 433 route errors. ## Validation - Post-review remediation: - `cd apps/platform && php artisan test --filter=Spec433 --compact` - Result: PASS, 48 tests, 211 assertions. - `cd apps/platform && php artisan test --filter='Spec433|Spec432|Spec431' --compact` - Result: PASS, 150 tests, 1057 assertions. - `cd apps/platform && ./vendor/bin/pint --dirty` - Result: PASS, 10 files. - `git diff --check` - Result: PASS. - `cd apps/platform && php artisan test --filter=Spec433 --compact` - Result: PASS, 30 tests, 157 assertions. - `cd apps/platform && php artisan test --filter=Spec432 --compact` - Result: PASS, 72 tests, 430 assertions. - `cd apps/platform && php artisan test --filter=Spec431 --compact` - Result: PASS, 30 tests, 416 assertions. - `cd apps/platform && php artisan test --filter='Spec433|Spec432|Spec431' --compact` - Result: PASS, 132 tests, 1003 assertions. - `cd apps/platform && php artisan test --filter='Spec433|Spec432|Spec431|Spec430|Spec427ExchangeTeamsNoEvidencePromotion|Spec427ExchangeTeamsNoCompareRenderCertification|Spec426ExchangeTeamsNoTenantId|Spec426ExchangeTeamsNoMiniPlatform|Spec417IdentityNoLegacyNoUiActivation|Spec419M365RegistryExpansion|Spec420M365NoOverclaim|ProviderCapabilityRegistryTest|ProviderCapabilityEvaluationTest' --compact` - Result: PASS, 222 tests, 2028 assertions, 1 PostgreSQL-only skip. - `cd apps/platform && ./vendor/bin/pint --dirty` - Result: PASS, 10 files. - `cd apps/platform && ./vendor/bin/pint app/Services/TenantConfiguration/ExchangePowerShellInvocationReadinessEvaluator.php tests/Feature/TenantConfiguration/Spec433ExchangePowerShellEvidenceReadinessTest.php tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php tests/Feature/TenantConfiguration/Spec432ExchangePowerShellProductionRunnerGateTest.php --format=txt` - Result: PASS. - `git diff --check` - Result: PASS. - `cd apps/platform && ./vendor/bin/sail artisan migrate --force` - Result: PASS, nothing to migrate. - `cd apps/platform && ./vendor/bin/sail artisan config:clear` - Result: PASS after browser override removal. ## Deferred Work - Content-backed Exchange evidence capture/promotion remains Spec 434 or later. - Live Exchange execution success, customer claims, compare/render/certification/restore readiness, Teams, Exchange Admin API, broader Exchange targets, and UI productization remain out of scope. - A future UI/product-surface cleanup should decide how to distinguish generic provider readiness from Exchange PowerShell-specific capability readiness on provider connection pages.