TenantAtlas/specs/431-exchange-powershell-invocation-operation-registration-gate/implementation-report.md
ahmido 9374260ae1 feat: add Exchange PowerShell invocation gate (#498)
## Summary
- Adds the Spec 431 Exchange PowerShell invocation gate and operation registration slice.
- Introduces trusted provider operation start handling and read-only Exchange PowerShell runner abstractions.
- Updates provider capability/readiness evaluation and coverage for Exchange PowerShell invocation safety.

## Verification
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Providers/ProviderCapabilityEvaluationTest.php tests/Feature/Providers/ProviderOperationCapabilityGateTest.php tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php tests/Unit/Providers/ProviderCapabilityRegistryTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php tests/Unit/Support/TenantConfiguration/Spec431ExchangePowerShellOperationRegistrationTest.php tests/Unit/Verification/ManagedEnvironmentPermissionCapabilityMappingTest.php`
- Result: 80 passed, 685 assertions.

## Product Surface / Ops
- Rendered UI surface changed: N/A.
- Filament/Livewire surface changed: N/A.
- Deployment impact: config/runtime service changes only; no migrations, queues, or assets added.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #498
2026-07-07 15:23:29 +00:00

151 lines
14 KiB
Markdown

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