# Implementation Report: Exchange PowerShell Production Runner Boundary and Runtime Gate ## Result - **Gate result**: PASS. - **Active spec**: `specs/432-exchange-powershell-production-runner-boundary-runtime-gate/`. - **Branch**: `432-exchange-powershell-production-runner-boundary-runtime-gate`. - **HEAD at implementation start**: `9374260a feat: add Exchange PowerShell invocation gate (#498)`. - **Initial dirty state**: only untracked active Spec 432 package. - **Final dirty state**: application/config/test changes plus the active Spec 432 package and this report. - **Analysis/fix iterations**: 4. Iteration 1 found one bounded in-scope timeout-cleanup hardening issue in the Symfony process executor. Iteration 2 added missing direct credential and permission-evidence matrix coverage. Iteration 3 fixed the invalid-enum fixture by using a raw DB corruption fixture for unexpected stored credential kinds. Iteration 4 closed the final manual review findings: production runner binding now requires both invocation and production gates, command construction no longer generates a pipeline/script conversion string, provider-lock conflict coverage was added, the requirements checklist was closed, and Pint was rerun after formatting. ## Activated Skills and Gates - `spec-kit-implementation-loop`: active implementation workflow. - TenantPilot repo gates: spec readiness, workspace-scope safety, RBAC/action safety, OperationRun truth, provider freshness semantics. - `pest-testing`: Pest 4 unit/feature coverage. - Browser skill: not activated; no rendered UI surface changed. ## Implementation Summary - Added explicit `tenantpilot.features.exchange_powershell_production_runner`, default `false`. - Kept `ExchangePowerShellCommandRunner` defaulting to `DisabledExchangePowerShellCommandRunner`; production runner is selected only when both the invocation flag and production-runner flag are enabled. - Added a production runner boundary with runtime readiness, credential-reference, permission-evidence, provider-scope, command-builder, argument-vector process, output, timeout, concurrency, cleanup, and redaction gates. - Added non-invasive runtime readiness checks for PowerShell availability and ExchangeOnlineManagement module presence through a fakeable process executor. - Added reference-only credential handling. Secret-based app credentials block by default; certificate/federated/managed identity remain fail-closed unless explicitly configured as supported reference kinds. - Added verified Exchange permission evidence evaluation using the existing `ManagedEnvironmentPermission` `Exchange.ManageAsApp` path. - Preserved OperationRun-only execution truth and context-only `provider_connection_id`; no new column, migration, evidence row, route, UI, job, schedule, listener, or `tenant_id`. ## Prerequisite Proof - Spec 430 proof retained: only `transportRule`, `remoteDomain`, and `inboundConnector` command contracts are allowed. - Spec 431 proof retained: invocation remains OperationRun-owned, fake runner remains testable, direct provider start gate remains blocked, disabled default remains valid, no evidence/UI/migration/tenant-id promotion exists. - Completed historical specs were not rewritten. ## Runner and Credential Matrix | Case | Result | | --- | --- | | Default config | Disabled runner binding | | Invocation flag false + production flag true | OperationRun blocked before process execution | | Production flag false + invocation flag true | OperationRun blocked before runner/process execution | | No credential reference | Blocked, sanitized `credential_state=missing` | | Secret-based app credential | Blocked, no credential payload/material persisted | | Certificate reference default | Blocked as unsupported | | Certificate reference explicitly supported for test | Advances to runtime/process/output gates | | Missing Exchange permission evidence | Blocked by provider capability before runner execution | | Verified Exchange permission evidence | Advances only to next gate; does not promote evidence/customer claims | ## Target and No-Promotion Matrix | Target | Command | Scope | | --- | --- | --- | | `transportRule` | `Get-TransportRule` | read-only, internal runner boundary | | `remoteDomain` | `Get-RemoteDomain` | read-only, internal runner boundary | | `inboundConnector` | `Get-InboundConnector` | read-only, internal runner boundary | No Exchange evidence rows, normalized payloads, comparable/renderable/certified/restore-ready states, Review Pack output, reports, readiness badges, customer claims, routes, navigation, global search, assets, jobs, schedules, listeners, migrations, or Exchange mini-platform were introduced. ## Safety Proof - Runtime checks use fixed argument vectors with `-NoLogo`, `-NoProfile`, and `-NonInteractive`. - Readiness checks do not run `Connect-ExchangeOnline`, install modules, import tenant sessions, dump environment variables, or prompt interactively. - Command construction is from verified contracts only; mutation families, unknown commands, shell fragments, pipelines, redirection, aliases, and unknown parameters are rejected before process execution. - Production process commands pass only the allowlisted `Get-*` command name as an argument-vector item; JSON serialization/pipeline scripting remains out of scope and live non-JSON output fails safely through the output guard. - Process execution accepts structured command objects only. - Output guard rejects non-zero exits, timeouts, oversized stdout/stderr, non-UTF8/binary output, scalar/text/malformed JSON, warning-prefixed output, and item-count overflow. - Timeout cleanup calls process stop when Symfony raises a timeout exception. - Provider and workspace locks are released after success, blocked, failed, timeout, provider-lock conflict, workspace-lock conflict, or executor-exception-result paths covered by focused tests. - Stale-lock handling uses the existing cache lock TTL; no separate stale-lock subsystem was introduced because the repo has no narrower Exchange runner pattern and a new subsystem would exceed this slice. - Temp files are not used. - Summary counts remain flat numeric allowed keys: `total`, `processed`, `succeeded`, `failed`, `skipped`, `items`. - No new summary key was introduced, so `OperationSummaryKeys::all()` was unchanged. ## Product Surface Contract Close-Out - **No-legacy posture**: canonical addition only; no fallback readers, aliases, hidden routes, duplicate UI, or legacy shims. - **Product Surface Impact**: N/A - no rendered product surface changed. - **UI Surface Impact**: N/A - no rendered UI surface changed. - **Browser proof**: N/A - no rendered UI surface changed. - **Human Product Sanity**: N/A - no product surface changed. - **Visible complexity outcome**: neutral; complexity is backend safety boundary only. - **Livewire v4 compliance**: unchanged repo baseline; no Livewire code added. - **Provider registration location**: no Filament/panel provider change; Laravel providers remain in `apps/platform/bootstrap/providers.php`. - **Global search**: no Filament resources or global search behavior changed. - **Destructive/high-impact actions**: no UI actions added; production runner remains backend-gated and read-only `Get-*` only. - **Asset strategy**: no assets; `filament:assets` not required for this slice. - **Deployment impact**: no migrations, queues, schedules, storage, or assets. New env gates default disabled: - `TENANTPILOT_EXCHANGE_POWERSHELL_PRODUCTION_RUNNER_ENABLED=false` - runtime limit/env vars under `TENANTPILOT_EXCHANGE_POWERSHELL_*` - supported reference kinds default empty. ## Validation - `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec432 --compact` - **Result**: local Sail harness failed before tests with Symfony Process signal 9; non-Docker fallback was used for validation. - `cd apps/platform && php artisan test --filter=Spec432 --compact` - **Result**: PASS, 72 tests, 424 assertions. - `cd apps/platform && php artisan test --filter='Spec431|Spec430|Spec427ExchangeTeamsNoEvidencePromotion|Spec427ExchangeTeamsNoCompareRenderCertification|Spec426ExchangeTeamsNoTenantId|Spec426ExchangeTeamsNoMiniPlatform|Spec417IdentityNoLegacyNoUiActivation|Spec419M365RegistryExpansion|Spec420M365NoOverclaim|ProviderCapabilityRegistryTest' --compact` - **Result**: PASS, 115 tests, 1422 assertions, 1 PostgreSQL-only skip. - `cd apps/platform && php artisan config:cache && php artisan config:clear` - **Result**: PASS. - `cd apps/platform && ./vendor/bin/pint --dirty --format agent` - **Result**: PASS after formatting the two new Spec 432 test files. - `cd apps/platform && ./vendor/bin/pint --dirty --test --format agent` - **Result**: PASS. - `git diff --check` - **Result**: PASS. ## Deferred Work - Live Exchange Online success remains a non-goal. - Evidence promotion, compare/render/certification/restore/customer claims, and customer-visible readiness are follow-up specs. - Broader Exchange/Teams targets remain out of scope.