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
8.8 KiB
8.8 KiB
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, defaultfalse. - Kept
ExchangePowerShellCommandRunnerdefaulting toDisabledExchangePowerShellCommandRunner; 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
ManagedEnvironmentPermissionExchange.ManageAsApppath. - Preserved OperationRun-only execution truth and context-only
provider_connection_id; no new column, migration, evidence row, route, UI, job, schedule, listener, ortenant_id.
Prerequisite Proof
- Spec 430 proof retained: only
transportRule,remoteDomain, andinboundConnectorcommand 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:assetsnot 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.