# Implementation Report: Spec 431 - Exchange PowerShell Invocation Operation Registration and Execution Gate ## Status Implemented on 2026-07-05. Backend-only operation registration and fake-runner invocation gate are complete for exactly `transportRule`, `remoteDomain`, and `inboundConnector`. ## Preflight - **Active spec**: `specs/431-exchange-powershell-invocation-operation-registration-gate/` - **Implementation branch**: `431-exchange-powershell-invocation-operation-registration-gate-session-1783272254` - **HEAD before implementation**: `9b58a569 feat: add Exchange PowerShell adapter contract slice 1 (#497)` - **Initial dirty state**: untracked active Spec 431 package only. - **Activated skills/gates**: `spec-kit-implementation-loop`, `pest-testing`, `workflows/spec-readiness-gate`, `repo-contracts/workspace-scope-safety`, `repo-contracts/rbac-action-safety`, `repo-contracts/operation-run-truth`, `repo-contracts/provider-freshness-semantics`. - **Hard-gate stop conditions checked**: no live provider execution, no shell/PowerShell process, no evidence, no UI, no migration, no scheduled capture/job, no `tenant_id`, no Exchange mini-platform. ## Files Changed Runtime: - `apps/platform/app/Support/OperationRunType.php` - `apps/platform/app/Support/OperationCatalog.php` - `apps/platform/app/Support/Operations/OperationRunCapabilityResolver.php` - `apps/platform/app/Support/Providers/Capabilities/ProviderCapabilityRegistry.php` - `apps/platform/app/Services/Providers/ProviderOperationRegistry.php` - `apps/platform/app/Services/Providers/ProviderConnectionResolver.php` - `apps/platform/app/Services/Providers/ProviderOperationStartGate.php` - `apps/platform/app/Services/Providers/ProviderOperationTrustedStarter.php` - `apps/platform/app/Support/Providers/Capabilities/ProviderCapabilityEvaluator.php` - `apps/platform/app/Providers/AppServiceProvider.php` - `apps/platform/config/tenantpilot.php` - `apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationGate.php` - `apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandContract.php` - `apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandRunner.php` - `apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationContext.php` - `apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationResult.php` - `apps/platform/app/Services/TenantConfiguration/FakeExchangePowerShellCommandRunner.php` - `apps/platform/app/Services/TenantConfiguration/DisabledExchangePowerShellCommandRunner.php` Tests: - `apps/platform/tests/Unit/Providers/ProviderCapabilityRegistryTest.php` - `apps/platform/tests/Feature/Providers/ProviderCapabilityEvaluationTest.php` - `apps/platform/tests/Feature/Providers/ProviderOperationCapabilityGateTest.php` - `apps/platform/tests/Unit/Providers/ProviderOperationStartGateTest.php` - `apps/platform/tests/Unit/ManagedEnvironmentPermissionCheckClustersTest.php` - `apps/platform/tests/Unit/Support/TenantConfiguration/Spec431ExchangePowerShellOperationRegistrationTest.php` - `apps/platform/tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php` Spec artifacts: - `specs/431-exchange-powershell-invocation-operation-registration-gate/tasks.md` - `specs/431-exchange-powershell-invocation-operation-registration-gate/implementation-report.md` ## Proportionality Review - **New abstractions added**: one public gate, one internal trusted provider-operation starter, one command contract DTO, one runner interface, one invocation context DTO, one result DTO, one fake runner, one disabled runner. - **Why needed**: the gate centralizes authorization, provider scope, source-contract state, feature flag, credential policy, operation lifecycle, shape validation, and redaction. The runner interface keeps command execution structured and typed, making raw shell/string invocation impossible at the contract boundary. - **Existing mechanisms extended**: `OperationRunService`, `ProviderOperationStartGate`, `ProviderOperationRegistry`, `ProviderCapabilityRegistry`, `OperationRunCapabilityResolver`, `CoverageSourceContractResolver`, and `ExchangePowerShellCommandContracts`. - **Why no smaller change was sufficient**: wiring the fake runner directly to tests would not prove OperationRun-before-runner ordering, provider scope/capability gates, feature default-off behavior, or bypass prevention. - **Mini-platform avoidance**: no Exchange namespace, no adapter framework, no route/UI/job/migration/evidence writer, no provider-client dependency, no Teams or wider Exchange catalog. ## Operation Registration Proof - **OperationRun type**: `tenant_configuration.exchange_powershell_invocation` added to `OperationRunType`. - **OperationCatalog**: canonical definition and canonical alias added with internal label `Exchange PowerShell invocation`. - **ProviderOperationRegistry**: added `exchange_powershell` module definition requiring `Capabilities::PROVIDER_RUN` and provider capability key `exchange_powershell_invoke`; Microsoft binding explicitly states fake-gated/no-evidence posture. - **ProviderCapabilityRegistry**: added `exchange_powershell_invoke` with `provider.exchange_powershell_invocation` plus `permissions.admin_consent` requirements. - **Execution capability**: `OperationRunCapabilityResolver` maps the invocation type to `Capabilities::PROVIDER_RUN`. - **Feature flag**: `tenantpilot.features.exchange_powershell_invocation`, default `false` through `TENANTPILOT_EXCHANGE_POWERSHELL_INVOCATION_ENABLED`. ## Invocation Gate Proof - **Single public entry point**: `ExchangePowerShellInvocationGate::invoke()`. - **Direct provider-start bypass**: public `ProviderOperationStartGate::start()` always blocks `tenant_configuration.exchange_powershell_invocation` with `ext.exchange_powershell_invocation_gate_required`. The invocation gate uses an explicit internal `ProviderOperationTrustedStarter`, so caller-supplied context cannot unlock this operation through the public start boundary. - **Direct runner bypass**: `ExchangePowerShellCommandRunner::run()` accepts only `ExchangePowerShellCommandContract` and `ExchangePowerShellInvocationContext`. The typed contract is created only after full canonical contract and parameter validation, stores the canonical allowlisted contract rather than caller-mutated arrays, and the runner receives no separate raw parameter array; tests prove raw command strings, raw arrays, mutating contract arrays, mutated safety metadata, unexpected contract fields, and unverified parameters fail before runner execution. - **Spec 430 source gate**: requires `contract_verified_pending_capture`, `adapter_contract_available`, `provider_calls_allowed = false`, and `execution_enabled = false` from `CoverageSourceContractResolver`. - **Target/command gate**: exactly `transportRule`, `remoteDomain`, `inboundConnector`; exactly `Get-TransportRule`, `Get-RemoteDomain`, `Get-InboundConnector`. - **Provider scope**: explicit workspace and managed-environment match on `ProviderConnection`; out-of-workspace/out-of-scope results stay 404. - **RBAC**: actor requires `Capabilities::PROVIDER_RUN`; readonly in-scope members get 403. - **Provider capability**: provider readiness still flows through the internal `ProviderOperationTrustedStarter` and `ProviderCapabilityEvaluator`, using connection-scoped readiness evidence. Fake mode bypasses only provider identity resolution, not scope, consent, provider binding, provider health freshness, or provider capability evidence. - **No shared verification surface promotion**: the Exchange PowerShell permission requirement remains internal to provider capability evaluation and is not registered as a shared `VerificationReport` check or rendered Required Permissions product-surface claim. - **Credential source**: non-fake mode uses existing `ProviderIdentityResolver` / `ProviderConnection::credential` / `ProviderCredential`. Fake mode uses `provider_identity_resolution_mode = fake_runner_bypass` only on the trusted Exchange invocation operation after scope, enabled-state, consent, and provider capability checks; generic provider operation starts still require provider identity. - **Operation lifecycle**: OperationRun is created by the internal provider-operation starter before runner execution; status/outcome transitions are made through `OperationRunService`. ## Runner, Shape, Redaction, And Failure Proof - **Fake runner**: supports success, empty success, auth failure, authorization failure, timeout, throttling, module unavailable, command unavailable, malformed scalar/text payload, and unexpected exception. - **Production runner**: `DisabledExchangePowerShellCommandRunner`, bound by default in `AppServiceProvider`, returns blocked `execution_blocked_runner_disabled`; no shell or Microsoft call exists. - **Summary counts**: only existing keys are emitted: `total`, `processed`, `succeeded`, `failed`, `skipped`, `items`. - **No new summary vocabulary**: `OperationSummaryKeys` unchanged; unknown-key behavior remains handled by `SummaryCountsNormalizer`. - **Failure reasons**: use existing provider reason codes, with local failure codes in sanitized OperationRun context/failure summaries. - **Shape validation**: successful runner payload must be a list of arrays and each non-empty item must include a required or derived identity candidate field. Empty success is valid only from a success envelope. - **Malformed output**: scalar/text output fails safely with `execution_failed_shape_unsafe`. - **Redaction**: OperationRun context stores command/resource metadata, counts, reason codes, and safe contract metadata only; tests assert no raw stdout/stderr/provider payload/transcript/token/secret promotion. ## No-Promotion / No-Surface Proof - **No evidence**: tests assert `TenantConfigurationResource` and `TenantConfigurationResourceEvidence` rows remain absent. - **No payload promotion**: no raw or normalized evidence payload is written. - **No customer/product claim**: no content-backed, comparable, renderable, certified, restore-ready, customer-ready, report, Review Pack, or customer output state is introduced. - **No UI**: no route, Filament page/resource, Livewire component, navigation, global search, asset, or browser surface introduced. - **No migration**: no database migration and no `operation_runs.provider_connection_id` column; provider connection id remains sanitized OperationRun context metadata. - **No tenant_id**: new Spec 431 runtime source is guarded for `tenant_id`, `provider_tenant_id`, and `entra_tenant_id`. - **No mini-platform**: no Exchange-specific service namespace/table/job/legacy shim/fallback reader/dual-write path. ## Product Surface And Filament Close-Out - **Product Surface Impact**: no rendered product surface changed. - **UI Surface Impact**: none. - **Page archetype/surface budgets/Technical Annex/deep-link demotion/canonical status vocabulary**: `N/A - no rendered UI surface changed`. - **Product Surface exceptions**: none. - **Focused browser proof**: `N/A - no rendered UI surface changed`. - **Human Product Sanity**: `N/A - no product surface changed`. - **Visible complexity outcome**: neutral. - **No-legacy posture**: no legacy shim, fallback reader, dual truth, old route, duplicate UI, or mini-platform. - **Livewire v4 compliance**: no Livewire runtime code changed; repo baseline remains Livewire v4.1.4. - **Provider registration location**: no Filament panel provider registration changed; Laravel panel providers remain in `apps/platform/bootstrap/providers.php`. One service binding was added in `AppServiceProvider`. - **Global search**: no Filament resource/global search surface changed. - **Destructive/high-impact actions**: none added. - **Asset strategy**: no assets registered; `filament:assets` is not required for this slice. - **Deployment impact**: no migrations, queues, scheduler, storage, assets, or Dokploy/container runtime change. New env flag is optional and defaults disabled. ## Validation Successful: - `cd apps/platform && ./vendor/bin/pint --dirty` - `cd apps/platform && php artisan test --filter=Spec431 --compact` Result: 30 passed, 416 assertions. - `cd apps/platform && php artisan test tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php --compact` Result: 28 passed, 120 assertions. - `cd apps/platform && php artisan test tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php --compact` Result: 39 passed, 220 assertions. - `cd apps/platform && php artisan test tests/Unit/Providers/ProviderCapabilityRegistryTest.php tests/Unit/Providers/ProviderOperationRegistryCanonicalTypeTest.php tests/Feature/TenantConfiguration/Spec430ExchangePowerShellNoPromotionTest.php tests/Feature/TenantConfiguration/Spec426ExchangeTeamsNoMiniPlatformTest.php tests/Feature/TenantConfiguration/Spec427ExchangeTeamsNoEvidencePromotionTest.php tests/Feature/TenantConfiguration/Spec427ExchangeTeamsNoTenantIdTest.php tests/Unit/Support/TenantConfiguration/Spec417CoverageIdentityStrategyRegistryTest.php tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php --compact` Result: 20 passed, 1 PostgreSQL-only Spec 419 check skipped, 688 assertions. - `cd apps/platform && php artisan test tests/Feature/Providers/ProviderCapabilityEvaluationTest.php tests/Feature/Providers/ProviderOperationCapabilityGateTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/ManagedEnvironmentPermissionCheckClustersTest.php tests/Unit/Providers/Spec394ProviderReadinessResolverTest.php tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php tests/Unit/Verification/ManagedEnvironmentPermissionCapabilityMappingTest.php --compact` Result: 58 passed, 299 assertions. - `git diff --check` Attempted / noted: - `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec431 --compact` was killed by signal 9 before producing test results. Non-Docker fallback was used afterward. - The previously noted adjacent provider-start fixture failures were resolved; the focused provider capability/start/freshness suite now passes in non-Docker PHP. ## Deferred Work - Production Exchange PowerShell execution hardening remains out of scope. - Evidence capture/promotion for Exchange remains out of scope and should be a separate spec after runtime/permission proof. - Teams invocation/evidence work remains separate. - No staging/production feature enablement is included; the feature flag defaults disabled.