TenantAtlas/specs/432-exchange-powershell-production-runner-boundary-runtime-gate/implementation-report.md
ahmido f4e342121a feat: add Exchange PowerShell production runner gate (#499)
Spec 432: Exchange PowerShell production runner boundary and runtime gate. Validation: php artisan test --filter=Spec432 --compact; ./vendor/bin/pint --dirty --test --format agent; git diff --check.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #499
2026-07-07 18:34:18 +00:00

115 lines
8.8 KiB
Markdown

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