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

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