TenantAtlas/specs/432-exchange-powershell-production-runner-boundary-runtime-gate/plan.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

322 lines
25 KiB
Markdown

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