# Implementation Plan: Exchange PowerShell Production Runner Boundary and Runtime Gate **Branch**: `432-exchange-powershell-production-runner-boundary-runtime-gate` | **Date**: 2026-07-07 | **Spec**: `specs/432-exchange-powershell-production-runner-boundary-runtime-gate/spec.md` **Input**: Feature specification from `specs/432-exchange-powershell-production-runner-boundary-runtime-gate/spec.md` ## Summary Implement a production-runner boundary and runtime gate for the existing Exchange PowerShell invocation path covering exactly `transportRule`, `remoteDomain`, and `inboundConnector`. Disabled runner remains the default. Production runner selection requires explicit invocation and production-runner config gates plus runtime readiness, credential reference, Exchange permission evidence, provider scope, OperationRun ownership, command safety, process execution safety, output safety, timeout/concurrency controls, and redaction. Live invocation may remain blocked when supported credential or verified permission evidence is absent. No evidence promotion, UI, routes, jobs, schedules, listeners, migrations, customer claims, or `tenant_id` are allowed. ## Technical Context **Language/Version**: PHP 8.4 / Laravel 12 **Primary Dependencies**: Filament v5 / Livewire v4 baseline remains unchanged; Symfony Process may be used if already available through Laravel dependencies **Storage**: PostgreSQL via existing `operation_runs` only; no migration **Testing**: Pest 4 focused unit and feature tests **Validation Lanes**: fast-feedback/confidence focused tests, selected regressions, browser N/A **Target Platform**: Laravel Sail local, Dokploy staging/production with production runner disabled by default **Project Type**: Laravel monolith under `apps/platform` **Performance Goals**: No unbounded process execution; per-invocation timeout and concurrency locks must bound runtime work **Constraints**: No raw shell strings, no Microsoft calls during readiness checks, no module install, no credential material persistence, no provider payload persistence, no rendered UI **Scale/Scope**: Exactly three Exchange target types and one existing invocation operation ## Preflight Findings - Current prep branch before Spec Kit execution: `platform-dev`. - HEAD before Spec Kit execution: `9374260a feat: add Exchange PowerShell invocation gate (#498)`. - Initial dirty state before Spec Kit execution: clean. - Spec Kit helper created branch: `432-exchange-powershell-production-runner-boundary-runtime-gate`. - Existing related packages: `specs/430-exchange-powershell-adapter-contract-slice-1/` and `specs/431-exchange-powershell-invocation-operation-registration-gate/`. - No existing `specs/432-*` package was found before preparation. - Spec 430 implementation report proves the three command contracts and no live provider/evidence/UI/migration scope. - Spec 431 implementation report proves OperationRun/provider operation registration, `exchange_powershell_invoke` capability, fake runner proof, disabled runner default, sanitized context, no evidence, no UI, no migration, and no `tenant_id`. - Current `AppServiceProvider` binds `ExchangePowerShellCommandRunner` to `DisabledExchangePowerShellCommandRunner`. - Current config has `tenantpilot.features.exchange_powershell_invocation` defaulting false through `TENANTPILOT_EXCHANGE_POWERSHELL_INVOCATION_ENABLED`. - Current `OperationSummaryKeys::all()` includes generic keys such as `total`, `processed`, `succeeded`, `failed`, `skipped`, and `items`; use these unless a new key is explicitly justified and tested. - `RunFailureSanitizer` already normalizes provider/auth/permission/timeout-like failures and redacts secret-like strings. ## UI / Surface Guardrail Plan - **Guardrail scope**: no operator-facing surface change. - **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: N/A. - **No-impact class, if applicable**: backend-only runtime and operation safety boundary. - **Native vs custom classification summary**: N/A. - **Shared-family relevance**: OperationRun/provider operation shared contracts only; no rendered UI shared family. - **State layers in scope**: OperationRun context, summary counts, failure reasons, runtime/credential/permission gate states. - **Audience modes in scope**: internal operator/reviewer only; no customer surface. - **Decision/diagnostic/raw hierarchy plan**: No product view. Raw process/provider output is forbidden from persistence. - **Raw/support gating plan**: Raw output and credential material are never stored. - **One-primary-action / duplicate-truth control**: N/A. - **Handling modes by drift class or surface**: N/A. - **Repository-signal treatment**: service-provider binding changes are backend service-container behavior only; panel provider registration remains unchanged. - **Special surface test profiles**: N/A. - **Required tests or manual smoke**: `N/A - no rendered UI surface changed`. - **Exception path and spread control**: none. - **Active feature PR close-out entry**: no rendered product surface changed. - **UI/Productization coverage decision**: No UI surface impact. - **Coverage artifacts to update**: none. - **No-impact rationale**: The spec forbids routes, Filament pages, Livewire components, navigation, global search, and assets. - **Navigation / Filament provider-panel handling**: no panel/provider registration changes; Laravel panel providers remain in `apps/platform/bootstrap/providers.php`. - **Screenshot or page-report need**: no. ## Product Surface Contract Plan - **Product Surface Contract reference**: `docs/product/standards/product-surface-contract.md`. - **No-legacy posture**: canonical addition only; no compatibility aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures. - **Page archetype and surface budget plan**: N/A. - **Technical Annex and deep-link demotion plan**: N/A - no rendered product surface changed. OperationRun remains internal/audit truth and no new links are added. - **Canonical status vocabulary plan**: N/A. - **Product Surface exceptions**: none. - **Browser verification plan**: `N/A - no rendered UI surface changed`. - **Human Product Sanity plan**: N/A. - **Visible complexity outcome target**: neutral for product surfaces. - **Implementation report target**: `specs/432-exchange-powershell-production-runner-boundary-runtime-gate/implementation-report.md`. ## Filament / Livewire / Deployment Posture - **Livewire v4 compliance**: repo baseline remains Livewire v4; no Livewire runtime code planned. - **Panel provider registration location**: no panel/provider change; Laravel panel providers remain in `apps/platform/bootstrap/providers.php`. - **Global search posture**: no Filament resource/global search surface changed. - **Destructive/high-impact action posture**: no UI action added; production runner remains backend-gated and read-only command only. - **Asset strategy**: no assets; `filament:assets` is not required for this slice. - **Testing plan**: no pages/widgets/relation managers/actions/browser tests because no rendered UI surface changes. - **Deployment impact**: no migrations, queues, scheduler, storage, assets, or browser build. New env/config gate for production runner defaults disabled and must be documented in the implementation report if added. ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes. - **Systems touched**: `ExchangePowerShellInvocationGate`, `ExchangePowerShellCommandRunner`, `DisabledExchangePowerShellCommandRunner`, `OperationRunService`, `ProviderOperationTrustedStarter`, `ProviderCapabilityEvaluator`, `ProviderCapabilityRegistry`, `ProviderOperationRegistry`, `OperationSummaryKeys`, `SummaryCountsNormalizer`, `RunFailureSanitizer`, provider credential/identity infrastructure, and config under `tenantpilot.features`. - **Shared abstractions reused**: OperationRun lifecycle, provider operation/capability registries, SummaryCountsNormalizer, RunFailureSanitizer, Spec 430 command contracts, Spec 431 invocation gate. - **New abstraction introduced? why?**: Production runner, runtime readiness checker, credential resolver/evaluator, permission evidence evaluator, process executor, command builder, and runtime policy may be introduced because process execution and credential safety require explicit testable boundaries. - **Why the existing abstraction was sufficient or insufficient**: Existing paths are sufficient for operation truth, provider scope, and fake/disabled runner proof. They are insufficient for production process execution, non-invasive runtime checks, credential/permission evidence gating, no-shell command construction, output guards, timeout/concurrency cleanup, and runner selection. - **Bounded deviation / spread control**: No generic PowerShell platform, no multi-provider process framework, no evidence writer, no UI. Keep new services under the repo-canonical TenantConfiguration service path unless implementation finds an existing narrower home. ## OperationRun UX Impact - **Touches OperationRun start/completion/link UX?**: yes, backend OperationRun lifecycle and sanitized context only. - **Central contract reused**: `OperationRunService`, `OperationSummaryKeys`, `SummaryCountsNormalizer`, `RunFailureSanitizer`, and Spec 431 invocation path. - **Delegated UX behaviors**: no toast, run link, artifact link, browser event, queued DB notification, or surface messaging. - **Surface-owned behavior kept local**: none. - **Queued DB-notification policy**: N/A. - **Terminal notification path**: central lifecycle mechanism where applicable; no feature-local notification. - **Exception path**: none. ## Provider Boundary & Portability Fit - **Shared provider/platform boundary touched?**: yes. - **Provider-owned seams**: Exchange command names, ExchangeOnlineManagement module readiness, Exchange permission evidence, PowerShell process command construction, response/output shape, and command safety. - **Platform-core seams**: OperationRun lifecycle, provider connection/scope, provider operation/capability gating, summary/failure sanitizer, credential-reference metadata, and config gating. - **Neutral platform terms / contracts preserved**: operation, provider connection, capability, managed environment, workspace, runtime readiness, credential reference, permission evidence, process executor, summary counts, failure reason, runner mode. - **Retained provider-specific semantics and why**: Exchange command/runtime semantics stay in provider-owned TenantConfiguration runner-boundary services because Spec 432 is explicitly Exchange PowerShell only. - **Bounded extraction or follow-up path**: document-in-feature; verified credential/permission evidence support, evidence promotion, compare/render/certification, Teams runtime, and customer output remain separate follow-up specs. ## Constitution Check - Inventory-first: no inventory, snapshot, backup, or evidence truth is changed. - Read/write separation: only read-only Exchange `Get-*` commands are in scope; mutation commands are rejected. - Graph contract path: no Microsoft Graph call path changed; no Graph call added. - Deterministic capabilities: provider/actor capability mappings remain central and tested through Spec 431 path. - RBAC-UX: non-member workspace/environment access is 404; member without capability is 403 or repo-equivalent; readonly cannot invoke. - Workspace isolation: workspace and managed environment are mandatory scope inputs. - Tenant isolation: provider connection and permission evidence must match managed environment before process execution. - Feature gates: existing invocation feature flag remains false by default; new production-runner flag must default false. - Credential source: use existing provider credential/identity reference truth; no new credential store. - Run observability: public invocation returns or references OperationRun/safe blocker through the existing invocation path. - OperationRun start UX: no rendered start surface; lifecycle/summary/failure paths stay central. - Ops-UX 3-surface feedback: no new UI feedback or notification policy. - Ops-UX lifecycle: status/outcome transitions only through `OperationRunService`. - Ops-UX summary counts: allowed keys only, flat numeric values only. - Data minimization: OperationRun context stores safe gate states and metadata only; no raw output, credentials, tokens, transcripts, or payloads. - Test governance: focused unit/feature tests with fake process executor and fake runtime-readiness seams; no browser lane and no CI dependency on host PowerShell or ExchangeOnlineManagement. - Proportionality: new runtime boundaries are justified by credential safety, shell/process safety, provider scope, and no-promotion truth. - No premature abstraction: no generic PowerShell framework or multi-provider process platform. - Persisted truth: no new tables or persisted evidence truth. - Behavioral state: blocker/failure codes change execution handling and remain bounded to runner safety. - Provider boundary: Exchange-specific terms remain provider-owned and do not become platform-core customer truth. - Product Surface Contract: N/A - no rendered product surface changed. ## Test Governance Check - **Test purpose / classification by changed surface**: Unit for binding/runtime/credential/permission/process/output/sanitizer behavior; Feature for OperationRun/provider scope/no evidence/no trigger/no migration/no `tenant_id`. - **Affected validation lanes**: fast-feedback/confidence focused tests and selected regressions; browser N/A. - **Why this lane mix is the narrowest sufficient proof**: The change is backend service/runtime safety behavior with no rendered UI and no schema. - **Narrowest proving command(s)**: - `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec432 --compact` - selected Spec 431 and Spec 430 regression tests - selected Spec 426/427/417/419/420 no-promotion, identity, registry, and generic evidence regressions - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `git diff --check` - **Fixture / helper / factory / seed / context cost risks**: workspace, managed environment, provider connection, credential reference, permission evidence, OperationRun, and fake process executor setup. Keep helpers explicit and feature-local unless reused intentionally. - **Expensive defaults or shared helper growth introduced?**: no planned defaults. - **Heavy-family additions, promotions, or visibility changes**: none planned; PostgreSQL-only lock proof must be named if implementation requires it. - **Surface-class relief / special coverage rule**: backend-only; browser `N/A - no rendered UI surface changed`. - **Closing validation and reviewer handoff**: implementation report must list exact tests and pass/fail counts. - **Budget / baseline / trend follow-up**: none expected. - **Review-stop questions**: verify disabled default, no live invocation without all gates, no shell strings, no Microsoft calls during readiness, no credential material, no raw output, no evidence, no UI/trigger surface, no migration, no `tenant_id`, no mini-platform. - **Escalation path**: reject-or-split if implementation attempts live evidence, UI, routes/jobs/schedules/listeners, migrations, broader Exchange/Teams scope, or customer claims. - **Active feature PR close-out entry**: backend runner-boundary safety; no rendered UI surface changed. - **Why no dedicated follow-up spec is needed**: Routine runner-boundary test upkeep stays inside Spec 432. Credential/permission evidence support and evidence promotion are separate follow-up specs only when needed. ## Project Structure ### Documentation (this feature) ```text specs/432-exchange-powershell-production-runner-boundary-runtime-gate/ |-- spec.md |-- plan.md |-- tasks.md |-- checklists/ | `-- requirements.md `-- implementation-report.md ``` ### Source Code (likely affected by later implementation) ```text apps/platform/app/Providers/AppServiceProvider.php apps/platform/config/tenantpilot.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationGate.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandRunner.php apps/platform/app/Services/TenantConfiguration/DisabledExchangePowerShellCommandRunner.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationContext.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationResult.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandContract.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandContracts.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellProductionRunner.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellRuntimeReadinessChecker.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCredentialReferenceResolver.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellPermissionEvidenceEvaluator.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellProcessCommandBuilder.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellProcessExecutor.php apps/platform/app/Services/TenantConfiguration/FakeExchangePowerShellProcessExecutor.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellRuntimePolicy.php apps/platform/app/Support/OpsUx/RunFailureSanitizer.php apps/platform/app/Support/OpsUx/OperationSummaryKeys.php apps/platform/app/Support/OpsUx/SummaryCountsNormalizer.php apps/platform/app/Support/Providers/Capabilities/ProviderCapabilityEvaluator.php apps/platform/tests/Unit/Support/TenantConfiguration/ apps/platform/tests/Feature/TenantConfiguration/ apps/platform/tests/Unit/OpsUx/ apps/platform/tests/Feature/Guards/ ``` Forbidden source changes: ```text apps/platform/routes/** apps/platform/resources/views/** apps/platform/app/Filament/** apps/platform/app/Livewire/** apps/platform/database/migrations/** new Exchange-specific evidence tables jobs/schedules/listeners that invoke the production runner customer report or Review Pack output ``` **Structure Decision**: Use the existing TenantConfiguration and OpsUx support paths. Do not create a new Exchange subsystem or base folder without explicit approval. ## Complexity Tracking | Violation | Why Needed | Simpler Alternative Rejected Because | | --- | --- | --- | | New runner boundary services | Live process execution requires explicit runtime, credential, permission, process, output, and redaction gates. | Extending the fake/disabled runner only would not prove production execution safety. | | New process executor abstraction | Tests must prove execution behavior without running PowerShell and without shell strings. | Direct Symfony Process use inside the runner would make fake testing and shell-string guard proof weaker. | | New failure states | Operators/reviewers need actionable blocked/failed reasons, and code needs behavior-specific handling. | Collapsing every failure to `unknown_error` would hide unsafe runtime, credential, permission, output, and concurrency conditions. | ## Proportionality Review - **Current operator problem**: Prevent false confidence that Exchange evidence is ready while allowing a safe production-runner boundary to be implemented and tested. - **Existing structure is insufficient because**: Spec 431 has only disabled/fake execution and no production process, credential, permission, runtime readiness, output, timeout, or concurrency proof. - **Narrowest correct implementation**: Add a safely blocked/gated production boundary for the three existing Exchange command contracts. - **Ownership cost created**: Focused service/test surface that future evidence specs must maintain and use. - **Alternative intentionally rejected**: Enabling live execution inside an evidence promotion spec, or using admin consent/feature flag alone as runtime permission proof. - **Release truth**: Current-release safety truth before evidence promotion. ## Implementation Phases ### Phase 0 - Preflight - Capture branch, HEAD, and dirty state. - Confirm Spec 431 PASS and no open blocking bypass/capability/redaction findings. - Confirm current disabled runner binding and invocation feature flag. - Confirm current credential infrastructure and provider permission evidence path. - Confirm summary keys and sanitizer behavior. - Confirm no evidence/UI/migration/job/schedule/listener scope. ### Phase 1 - Runner Binding and Config - Preserve disabled runner as default. - Add production-runner config flag under a repo-canonical `tenantpilot.features` or nested Exchange PowerShell config path. - Prove default false behavior and repo-canonical config-cache binding stability, using actual `config:cache` when viable or a documented service-container/config-resolved simulation when that is the repo-safe test path. - Production runner may be selected only when invocation gate and production-runner gate are enabled and all other gates pass. ### Phase 2 - Runtime Readiness - Add non-invasive runtime readiness checker. - Check configured PowerShell executable availability without profile loading or shell strings; automated tests must fake the executor/path-check seam. - Check ExchangeOnlineManagement module availability only through a non-invasive fixed command or safe unknown/missing state; automated tests must not require the real module. - Validate runtime environment allowlist, timeout policy, process executor, and temp policy. - Forbid Microsoft connection, module install, env dump, tenant session import, and interactive prompts. ### Phase 3 - Credential and Permission Gates - Add safe credential reference resolver/evaluator. - Block client-secret credentials by default. - Test certificate, federated, managed-identity, missing, expired, inaccessible, unsupported, and unknown states. - Add Exchange permission evidence evaluator. - Preserve admin-consent-not-sufficient behavior. - If no canonical verified evidence exists, fail closed and document Spec 433 as follow-up. ### Phase 4 - Process Executor and Command Builder - Add process executor abstraction and fake executor. - Add command builder using argument vectors or structured process calls. - Build only from Spec 430 command contracts. - Reject shell strings, pipelines, script fragments, redirection, aliases, profile loading, mutation command families, and unknown parameters. ### Phase 5 - Production Runner Boundary - Add production runner service behind the binding/config gates. - Require OperationRun context, runtime readiness, credential reference, permission evidence, provider scope, and redaction policy before process execution. - Return transient runner envelopes only. - Persist no provider payload, stdout, stderr, transcript, or credential material. ### Phase 6 - Output, Timeout, Concurrency, and Cleanup Guards - Guard stdout/stderr byte limits, item limits, UTF-8, structured shape, warning-prefix, non-zero exit, scalar/text output, binary output, and unexpected exceptions. - Enforce per-invocation timeout and process termination/cleanup. - Enforce provider/workspace concurrency locks and cleanup on success/failure/timeout/exception. - Prefer no temp files; if used, prove safe content and cleanup. ### Phase 7 - OperationRun, Summary, Redaction, and No-Promotion - Store only safe OperationRun context. - Reuse existing summary keys unless a narrow tested key is unavoidable. - Add or map sanitized failure reasons. - Prove redaction across context, summaries, failures, runner envelopes, process output, exceptions, logs if testable, and temp files if any. - Prove no evidence, compare/render/certification, restore, customer claim, UI, trigger surface, migration, `tenant_id`, legacy shim, fallback reader, or Exchange mini-platform. ### Phase 8 - Regression and Report - Run focused Spec 432 tests. - Run selected Spec 431/430/426/427/417/419/420 regressions. - Run Pint and `git diff --check`. - Complete `implementation-report.md` with runner/credential/target/no-promotion matrices and validation results. ## Rollout and Deployment Considerations - New production-runner config must default disabled in all environments. - No migration, queue, scheduler, storage, asset, or browser build impact is planned. - Dokploy/staging validation is required before any future spec enables live invocation outside test/fake contexts. - If implementation adds environment variables, the implementation report must list names, defaults, and staging/production validation requirements. ## Risk Controls - Hard stop if production runner can activate by default. - Hard stop if client-secret credentials or admin consent alone enable live invocation. - Hard stop if raw PowerShell strings, mutation commands, or unknown parameters can execute. - Hard stop if raw output, secrets, provider payload, or transcripts persist. - Hard stop if evidence or customer/product claims are promoted. - Hard stop if routes, UI, jobs, schedules, listeners, migrations, or `tenant_id` are introduced without amending the spec.