TenantAtlas/specs/431-exchange-powershell-invocation-operation-registration-gate/implementation-report.md
ahmido 9374260ae1 feat: add Exchange PowerShell invocation gate (#498)
## Summary
- Adds the Spec 431 Exchange PowerShell invocation gate and operation registration slice.
- Introduces trusted provider operation start handling and read-only Exchange PowerShell runner abstractions.
- Updates provider capability/readiness evaluation and coverage for Exchange PowerShell invocation safety.

## Verification
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Providers/ProviderCapabilityEvaluationTest.php tests/Feature/Providers/ProviderOperationCapabilityGateTest.php tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php tests/Unit/Providers/ProviderCapabilityRegistryTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php tests/Unit/Support/TenantConfiguration/Spec431ExchangePowerShellOperationRegistrationTest.php tests/Unit/Verification/ManagedEnvironmentPermissionCapabilityMappingTest.php`
- Result: 80 passed, 685 assertions.

## Product Surface / Ops
- Rendered UI surface changed: N/A.
- Filament/Livewire surface changed: N/A.
- Deployment impact: config/runtime service changes only; no migrations, queues, or assets added.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #498
2026-07-07 15:23:29 +00:00

14 KiB

Implementation Report: Spec 431 - Exchange PowerShell Invocation Operation Registration and Execution Gate

Status

Implemented on 2026-07-05. Backend-only operation registration and fake-runner invocation gate are complete for exactly transportRule, remoteDomain, and inboundConnector.

Preflight

  • Active spec: specs/431-exchange-powershell-invocation-operation-registration-gate/
  • Implementation branch: 431-exchange-powershell-invocation-operation-registration-gate-session-1783272254
  • HEAD before implementation: 9b58a569 feat: add Exchange PowerShell adapter contract slice 1 (#497)
  • Initial dirty state: untracked active Spec 431 package only.
  • Activated skills/gates: spec-kit-implementation-loop, pest-testing, workflows/spec-readiness-gate, repo-contracts/workspace-scope-safety, repo-contracts/rbac-action-safety, repo-contracts/operation-run-truth, repo-contracts/provider-freshness-semantics.
  • Hard-gate stop conditions checked: no live provider execution, no shell/PowerShell process, no evidence, no UI, no migration, no scheduled capture/job, no tenant_id, no Exchange mini-platform.

Files Changed

Runtime:

  • apps/platform/app/Support/OperationRunType.php
  • apps/platform/app/Support/OperationCatalog.php
  • apps/platform/app/Support/Operations/OperationRunCapabilityResolver.php
  • apps/platform/app/Support/Providers/Capabilities/ProviderCapabilityRegistry.php
  • apps/platform/app/Services/Providers/ProviderOperationRegistry.php
  • apps/platform/app/Services/Providers/ProviderConnectionResolver.php
  • apps/platform/app/Services/Providers/ProviderOperationStartGate.php
  • apps/platform/app/Services/Providers/ProviderOperationTrustedStarter.php
  • apps/platform/app/Support/Providers/Capabilities/ProviderCapabilityEvaluator.php
  • apps/platform/app/Providers/AppServiceProvider.php
  • apps/platform/config/tenantpilot.php
  • apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationGate.php
  • apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandContract.php
  • apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandRunner.php
  • apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationContext.php
  • apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationResult.php
  • apps/platform/app/Services/TenantConfiguration/FakeExchangePowerShellCommandRunner.php
  • apps/platform/app/Services/TenantConfiguration/DisabledExchangePowerShellCommandRunner.php

Tests:

  • apps/platform/tests/Unit/Providers/ProviderCapabilityRegistryTest.php
  • apps/platform/tests/Feature/Providers/ProviderCapabilityEvaluationTest.php
  • apps/platform/tests/Feature/Providers/ProviderOperationCapabilityGateTest.php
  • apps/platform/tests/Unit/Providers/ProviderOperationStartGateTest.php
  • apps/platform/tests/Unit/ManagedEnvironmentPermissionCheckClustersTest.php
  • apps/platform/tests/Unit/Support/TenantConfiguration/Spec431ExchangePowerShellOperationRegistrationTest.php
  • apps/platform/tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php

Spec artifacts:

  • specs/431-exchange-powershell-invocation-operation-registration-gate/tasks.md
  • specs/431-exchange-powershell-invocation-operation-registration-gate/implementation-report.md

Proportionality Review

  • New abstractions added: one public gate, one internal trusted provider-operation starter, one command contract DTO, one runner interface, one invocation context DTO, one result DTO, one fake runner, one disabled runner.
  • Why needed: the gate centralizes authorization, provider scope, source-contract state, feature flag, credential policy, operation lifecycle, shape validation, and redaction. The runner interface keeps command execution structured and typed, making raw shell/string invocation impossible at the contract boundary.
  • Existing mechanisms extended: OperationRunService, ProviderOperationStartGate, ProviderOperationRegistry, ProviderCapabilityRegistry, OperationRunCapabilityResolver, CoverageSourceContractResolver, and ExchangePowerShellCommandContracts.
  • Why no smaller change was sufficient: wiring the fake runner directly to tests would not prove OperationRun-before-runner ordering, provider scope/capability gates, feature default-off behavior, or bypass prevention.
  • Mini-platform avoidance: no Exchange namespace, no adapter framework, no route/UI/job/migration/evidence writer, no provider-client dependency, no Teams or wider Exchange catalog.

Operation Registration Proof

  • OperationRun type: tenant_configuration.exchange_powershell_invocation added to OperationRunType.
  • OperationCatalog: canonical definition and canonical alias added with internal label Exchange PowerShell invocation.
  • ProviderOperationRegistry: added exchange_powershell module definition requiring Capabilities::PROVIDER_RUN and provider capability key exchange_powershell_invoke; Microsoft binding explicitly states fake-gated/no-evidence posture.
  • ProviderCapabilityRegistry: added exchange_powershell_invoke with provider.exchange_powershell_invocation plus permissions.admin_consent requirements.
  • Execution capability: OperationRunCapabilityResolver maps the invocation type to Capabilities::PROVIDER_RUN.
  • Feature flag: tenantpilot.features.exchange_powershell_invocation, default false through TENANTPILOT_EXCHANGE_POWERSHELL_INVOCATION_ENABLED.

Invocation Gate Proof

  • Single public entry point: ExchangePowerShellInvocationGate::invoke().
  • Direct provider-start bypass: public ProviderOperationStartGate::start() always blocks tenant_configuration.exchange_powershell_invocation with ext.exchange_powershell_invocation_gate_required. The invocation gate uses an explicit internal ProviderOperationTrustedStarter, so caller-supplied context cannot unlock this operation through the public start boundary.
  • Direct runner bypass: ExchangePowerShellCommandRunner::run() accepts only ExchangePowerShellCommandContract and ExchangePowerShellInvocationContext. The typed contract is created only after full canonical contract and parameter validation, stores the canonical allowlisted contract rather than caller-mutated arrays, and the runner receives no separate raw parameter array; tests prove raw command strings, raw arrays, mutating contract arrays, mutated safety metadata, unexpected contract fields, and unverified parameters fail before runner execution.
  • Spec 430 source gate: requires contract_verified_pending_capture, adapter_contract_available, provider_calls_allowed = false, and execution_enabled = false from CoverageSourceContractResolver.
  • Target/command gate: exactly transportRule, remoteDomain, inboundConnector; exactly Get-TransportRule, Get-RemoteDomain, Get-InboundConnector.
  • Provider scope: explicit workspace and managed-environment match on ProviderConnection; out-of-workspace/out-of-scope results stay 404.
  • RBAC: actor requires Capabilities::PROVIDER_RUN; readonly in-scope members get 403.
  • Provider capability: provider readiness still flows through the internal ProviderOperationTrustedStarter and ProviderCapabilityEvaluator, using connection-scoped readiness evidence. Fake mode bypasses only provider identity resolution, not scope, consent, provider binding, provider health freshness, or provider capability evidence.
  • No shared verification surface promotion: the Exchange PowerShell permission requirement remains internal to provider capability evaluation and is not registered as a shared VerificationReport check or rendered Required Permissions product-surface claim.
  • Credential source: non-fake mode uses existing ProviderIdentityResolver / ProviderConnection::credential / ProviderCredential. Fake mode uses provider_identity_resolution_mode = fake_runner_bypass only on the trusted Exchange invocation operation after scope, enabled-state, consent, and provider capability checks; generic provider operation starts still require provider identity.
  • Operation lifecycle: OperationRun is created by the internal provider-operation starter before runner execution; status/outcome transitions are made through OperationRunService.

Runner, Shape, Redaction, And Failure Proof

  • Fake runner: supports success, empty success, auth failure, authorization failure, timeout, throttling, module unavailable, command unavailable, malformed scalar/text payload, and unexpected exception.
  • Production runner: DisabledExchangePowerShellCommandRunner, bound by default in AppServiceProvider, returns blocked execution_blocked_runner_disabled; no shell or Microsoft call exists.
  • Summary counts: only existing keys are emitted: total, processed, succeeded, failed, skipped, items.
  • No new summary vocabulary: OperationSummaryKeys unchanged; unknown-key behavior remains handled by SummaryCountsNormalizer.
  • Failure reasons: use existing provider reason codes, with local failure codes in sanitized OperationRun context/failure summaries.
  • Shape validation: successful runner payload must be a list of arrays and each non-empty item must include a required or derived identity candidate field. Empty success is valid only from a success envelope.
  • Malformed output: scalar/text output fails safely with execution_failed_shape_unsafe.
  • Redaction: OperationRun context stores command/resource metadata, counts, reason codes, and safe contract metadata only; tests assert no raw stdout/stderr/provider payload/transcript/token/secret promotion.

No-Promotion / No-Surface Proof

  • No evidence: tests assert TenantConfigurationResource and TenantConfigurationResourceEvidence rows remain absent.
  • No payload promotion: no raw or normalized evidence payload is written.
  • No customer/product claim: no content-backed, comparable, renderable, certified, restore-ready, customer-ready, report, Review Pack, or customer output state is introduced.
  • No UI: no route, Filament page/resource, Livewire component, navigation, global search, asset, or browser surface introduced.
  • No migration: no database migration and no operation_runs.provider_connection_id column; provider connection id remains sanitized OperationRun context metadata.
  • No tenant_id: new Spec 431 runtime source is guarded for tenant_id, provider_tenant_id, and entra_tenant_id.
  • No mini-platform: no Exchange-specific service namespace/table/job/legacy shim/fallback reader/dual-write path.

Product Surface And Filament Close-Out

  • Product Surface Impact: no rendered product surface changed.
  • UI Surface Impact: none.
  • Page archetype/surface budgets/Technical Annex/deep-link demotion/canonical status vocabulary: N/A - no rendered UI surface changed.
  • Product Surface exceptions: none.
  • Focused browser proof: N/A - no rendered UI surface changed.
  • Human Product Sanity: N/A - no product surface changed.
  • Visible complexity outcome: neutral.
  • No-legacy posture: no legacy shim, fallback reader, dual truth, old route, duplicate UI, or mini-platform.
  • Livewire v4 compliance: no Livewire runtime code changed; repo baseline remains Livewire v4.1.4.
  • Provider registration location: no Filament panel provider registration changed; Laravel panel providers remain in apps/platform/bootstrap/providers.php. One service binding was added in AppServiceProvider.
  • Global search: no Filament resource/global search surface changed.
  • Destructive/high-impact actions: none added.
  • Asset strategy: no assets registered; filament:assets is not required for this slice.
  • Deployment impact: no migrations, queues, scheduler, storage, assets, or Dokploy/container runtime change. New env flag is optional and defaults disabled.

Validation

Successful:

  • cd apps/platform && ./vendor/bin/pint --dirty
  • cd apps/platform && php artisan test --filter=Spec431 --compact
    Result: 30 passed, 416 assertions.
  • cd apps/platform && php artisan test tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php --compact
    Result: 28 passed, 120 assertions.
  • cd apps/platform && php artisan test tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php --compact
    Result: 39 passed, 220 assertions.
  • cd apps/platform && php artisan test tests/Unit/Providers/ProviderCapabilityRegistryTest.php tests/Unit/Providers/ProviderOperationRegistryCanonicalTypeTest.php tests/Feature/TenantConfiguration/Spec430ExchangePowerShellNoPromotionTest.php tests/Feature/TenantConfiguration/Spec426ExchangeTeamsNoMiniPlatformTest.php tests/Feature/TenantConfiguration/Spec427ExchangeTeamsNoEvidencePromotionTest.php tests/Feature/TenantConfiguration/Spec427ExchangeTeamsNoTenantIdTest.php tests/Unit/Support/TenantConfiguration/Spec417CoverageIdentityStrategyRegistryTest.php tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php --compact
    Result: 20 passed, 1 PostgreSQL-only Spec 419 check skipped, 688 assertions.
  • cd apps/platform && php artisan test tests/Feature/Providers/ProviderCapabilityEvaluationTest.php tests/Feature/Providers/ProviderOperationCapabilityGateTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/ManagedEnvironmentPermissionCheckClustersTest.php tests/Unit/Providers/Spec394ProviderReadinessResolverTest.php tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php tests/Unit/Verification/ManagedEnvironmentPermissionCapabilityMappingTest.php --compact
    Result: 58 passed, 299 assertions.
  • git diff --check

Attempted / noted:

  • cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec431 --compact was killed by signal 9 before producing test results. Non-Docker fallback was used afterward.
  • The previously noted adjacent provider-start fixture failures were resolved; the focused provider capability/start/freshness suite now passes in non-Docker PHP.

Deferred Work

  • Production Exchange PowerShell execution hardening remains out of scope.
  • Evidence capture/promotion for Exchange remains out of scope and should be a separate spec after runtime/permission proof.
  • Teams invocation/evidence work remains separate.
  • No staging/production feature enablement is included; the feature flag defaults disabled.