# Implementation Plan: Exchange Evidence Capture Adapter and Content-Only Guard **Branch**: `434-exchange-evidence-capture-adapter-content-only-guard` | **Date**: 2026-07-08 | **Spec**: `specs/434-exchange-evidence-capture-adapter-content-only-guard/spec.md` **Input**: Feature specification from `specs/434-exchange-evidence-capture-adapter-content-only-guard/spec.md` ## Summary Build the internal Exchange PowerShell evidence capture adapter and guard layer that lets TenantPilot safely bridge verified runner output into existing Coverage v2 evidence storage without promoting beyond `content_backed`. The plan reuses `tenant_configuration.capture`, requires Spec 433 readiness, `exchange_powershell_invoke` provider capability support, and Spec 432 runner safety, blocks unsafe identity before writer append, keeps empty collections as zero-item execution outcomes only, preserves generic Graph capture, and adds no UI, migration, trigger, customer output, restore, compare/render, certification, or `tenant_id` ownership truth. ## Technical Context **Language/Version**: PHP 8.4, Laravel 12 **Primary Dependencies**: Laravel services/jobs/models, existing TenantConfiguration services, `ProviderCapabilityEvaluator`, `ExchangePowerShellInvocationReadinessEvaluator`, `ExchangePowerShellCommandRunner`, `ExchangePowerShellInvocationResult`, OperationRun support **Storage**: Existing PostgreSQL Coverage v2 tables only: `tenant_configuration_resources` and `tenant_configuration_resource_evidence`; no new table planned **Testing**: Pest 4 unit/feature tests **Validation Lanes**: fast-feedback/confidence focused tests; selected regressions; browser N/A **Target Platform**: Laravel monolith under `apps/platform` **Project Type**: Web application backend service slice **Performance Goals**: Adapter uses fake/test runner output and existing resource/evidence writes; no live provider performance target **Constraints**: No live Exchange calls in tests, no raw payload in OperationRun context/logs, summary counts flat numeric only, same-scope workspace/managed-environment/provider-connection enforcement, no new `CaptureOutcome` enum value without amending the spec **Scale/Scope**: Three Exchange target types only: `transportRule`, `remoteDomain`, `inboundConnector` ## 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 internal service/evidence guard. - **Native vs custom classification summary**: N/A. - **Shared-family relevance**: evidence/OperationRun/provider internals only; no rendered shared interaction family. - **State layers in scope**: none for UI. - **Audience modes in scope**: N/A. - **Decision/diagnostic/raw hierarchy plan**: no rendered hierarchy; raw/support data remains internal. - **Raw/support gating plan**: raw payloads remain only in existing evidence storage and must not enter OperationRun context, logs, notifications, or customer output. - **One-primary-action / duplicate-truth control**: N/A. - **Handling modes by drift class or surface**: hard-stop if implementation needs UI, route, navigation, trigger, report, customer output, or browser proof. - **Repository-signal treatment**: report-only for static no-UI/no-route/no-trigger proof; hard-stop if violated. - **Special surface test profiles**: N/A. - **Required tests or manual smoke**: static/feature guard tests; browser `N/A - no rendered UI surface changed`. - **Exception path and spread control**: none. - **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage: `N/A - no rendered UI surface changed`. - **UI/Productization coverage decision**: No UI surface impact. - **Coverage artifacts to update**: none. - **No-impact rationale**: Adapter/guard infrastructure only; no reachable UI surface changes. - **Navigation / Filament provider-panel handling**: no panel/provider registration or navigation change. - **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 exception. - **Page archetype and surface budget plan**: N/A - no page changed. - **Technical Annex and deep-link demotion plan**: OperationRun, raw evidence, source keys, command metadata, payloads, stdout/stderr, provider diagnostics, and logs remain internal and not default-visible. - **Canonical status vocabulary plan**: N/A for UI; internal failure/outcome labels remain backend-only. - **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. - **Implementation report target**: `specs/434-exchange-evidence-capture-adapter-content-only-guard/implementation-report.md`. ## Filament / Livewire / Deployment Posture - **Livewire v4 compliance**: unchanged repo baseline; no Livewire code planned. - **Panel provider registration location**: no Filament panel provider change; Laravel panel providers remain under `apps/platform/bootstrap/providers.php`. - **Global search posture**: no resource/global search behavior changed. - **Destructive/high-impact action posture**: no UI action or start surface added. - **Asset strategy**: no assets; `filament:assets` not required for this slice. - **Testing plan**: no pages/widgets/relation managers/actions/browser smoke. Backend unit/feature and static guard tests only. - **Deployment impact**: no env vars, migrations, queues, scheduler, storage, assets, or browser build planned. If implementation discovers one is required, stop and amend the spec/plan first. ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes, backend shared systems. - **Systems touched**: `GenericContentEvidenceCaptureService`, `CoverageSourceContractResolver`, `CoverageResourceUpserter`, `CoverageEvidenceWriter`, `ExchangePowerShellCommandContracts`, `ExchangePowerShellProductionRunner`, `ExchangePowerShellInvocationReadinessEvaluator`, `OperationSummaryKeys`, `SummaryCountsNormalizer`, `tenantpilot` Exchange config, and related tests. - **Shared abstractions reused**: Coverage v2 models/writer path, `tenant_configuration.capture`, Spec 430 command contracts, Spec 432 runner output guard, Spec 433 readiness evaluator, provider capability/readiness gates, OperationRun summary count normalizer. - **New abstraction introduced? why?**: likely a narrow Exchange evidence capture adapter and guard classes. They are justified because Exchange runner output is not Graph-backed and must not be forced through Graph capture or manual evidence writes. - **Why the existing abstraction was sufficient or insufficient**: Existing Coverage v2 storage and operation truth are sufficient. Existing generic capture is Graph-oriented and insufficient for Exchange PowerShell runner output. Existing writer can over-promote, so the Exchange path needs a content-only maximum. - **Bounded deviation / spread control**: Adapter is provider-owned and internal only. It must not become a generalized provider capture framework, UI vocabulary, persisted taxonomy, customer-output path, or Product Surface runtime framework. ## OperationRun UX Impact - **Touches OperationRun start/completion/link UX?**: no rendered UX. It touches internal operation type/context requirements. - **Central contract reused**: existing `tenant_configuration.capture` OperationRun lifecycle and summary-count conventions. - **Delegated UX behaviors**: N/A. - **Surface-owned behavior kept local**: none. - **Queued DB-notification policy**: N/A; no queued notification change. - **Terminal notification path**: unchanged central lifecycle behavior for capture operations. - **Exception path**: none. New operation type, start UX, queued notification, or run link behavior requires spec amendment/split. ## Provider Boundary & Portability Fit - **Shared provider/platform boundary touched?**: yes. - **Provider-owned seams**: Exchange command contracts, Exchange runner output, Exchange response shape, Exchange-specific identity/redaction handoff. - **Platform-core seams**: workspace/managed-environment/provider ownership, OperationRun truth, Coverage v2 evidence storage, identity safety, coverage level, claim boundary, customer-proof boundary. - **Neutral platform terms / contracts preserved**: provider connection, operation, capture outcome, evidence state, coverage level, identity state, claim state. - **Retained provider-specific semantics and why**: command names and source surface metadata are required to execute and audit the verified Exchange contracts safely. - **Bounded extraction or follow-up path**: document-in-feature for adapter details; follow-up spec for target promotion and durable collection proof. ## Constitution Check *GATE: Must pass before implementation. Re-check after implementation.* - Inventory-first: PASS. This slice creates only internal evidence capture infrastructure; later target promotion remains separate. - Read/write separation: PASS. No Microsoft tenant writes; no restore/remediation. - Graph contract path: PASS WITH NOTE. Graph capture remains via existing Graph contracts; Exchange must not fake Graph endpoints or use Graph provider gateway. - Deterministic capabilities: PASS. Spec 433 readiness and provider capability checks remain required. - RBAC-UX: PASS. No new UI/action; trusted internal flow must preserve existing authorization/scope. - Workspace isolation: PASS. Same workspace, managed environment, and provider connection are required before capture/append. - Tenant isolation: PASS. No `tenant_id`; environment-owned evidence stays workspace + managed-environment scoped. - Run observability: PASS. Reuses `tenant_configuration.capture`; no new OperationRun type. - OperationRun start UX: N/A for rendered UX. No local start UX. - Ops-UX 3-surface feedback: N/A for new UX. Existing capture lifecycle unchanged. - Ops-UX lifecycle: PASS. Runtime must use service-owned status/outcome transitions. - Ops-UX summary counts: PASS. Existing flat numeric keys only. - Ops-UX guards: PASS. Add or extend tests/static proof for summary/context safety. - Data minimization: PASS. Raw output forbidden outside existing evidence storage. - Test governance: PASS. Unit/Feature lanes only; browser N/A. - Proportionality: PASS. New adapter/guards are security and evidence correctness infrastructure. - No premature abstraction: PASS WITH CONDITION. Keep adapter local and narrow; no broad provider framework. - Persisted truth: PASS. No new persisted entity/table/artifact planned. - Behavioral state: PASS WITH CONDITION. New outcome/failure labels must change blocking/validation behavior and stay internal. - UI semantics: PASS. No UI semantics introduced. - Shared pattern first: PASS. Reuse Coverage v2, OperationRun, readiness, and summary patterns. - Provider boundary: PASS. Exchange semantics remain provider-owned. - V1 explicitness/few layers: PASS. Explicit local adapter/guards, no platform engine. - Spec discipline / bloat check: PASS. Proportionality review is in `spec.md`. - Product Surface Contract: PASS. No rendered UI surface changed; browser N/A. - Temporary TCM/Coverage v2 cutover guard: PASS WITH CONDITION. No customer/operator proof, no legacy adapter, no fallback reader, no `tenant_id`, no raw evidence default display. ## Test Governance Check - **Test purpose / classification by changed surface**: Unit and Feature for backend service/evidence behavior. - **Affected validation lanes**: fast-feedback/confidence; browser N/A. - **Why this lane mix is the narrowest sufficient proof**: Adapter/guard behavior is service/database behavior; no UI, browser, migration, or customer output. - **Narrowest proving command(s)**: - `cd apps/platform && ./vendor/bin/sail artisan test --filter=Spec434 --compact` - `cd apps/platform && ./vendor/bin/sail artisan test --filter='Spec433|Spec432|Spec431|Spec430|Spec415|Spec417|Spec419|Spec420|Spec426|Spec427|ProviderCapabilityRegistryTest|ProviderCapabilityEvaluationTest' --compact` - `cd apps/platform && ./vendor/bin/pint --dirty --test` - `git diff --check` - **Fixture / helper / factory / seed / context cost risks**: keep fake runner output and adapter fixtures local; no broad workspace/provider default helpers. Unsafe-identity writer non-reachability is proven by code order plus zero resource/evidence assertions, not a literal writer spy. - **Expensive defaults or shared helper growth introduced?**: no. - **Heavy-family additions, promotions, or visibility changes**: none. - **Surface-class relief / special coverage rule**: browser `N/A - no rendered UI surface changed`. - **Closing validation and reviewer handoff**: Re-run Spec 434 focus and selected regressions; verify no UI/migration/trigger/customer output in diff. - **Budget / baseline / trend follow-up**: none expected. - **Review-stop questions**: stop if tests need live provider, shell, browser, broad fixture defaults, or schema. - **Escalation path**: reject-or-split if scope expands beyond adapter/guard infrastructure. - **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage. - **Why no dedicated follow-up spec is needed**: This spec is the dedicated adapter/guard prerequisite. Target promotion is a separate follow-up. ## Project Structure ### Documentation (this feature) ```text specs/434-exchange-evidence-capture-adapter-content-only-guard/ ├── spec.md ├── plan.md ├── tasks.md └── checklists/ └── requirements.md ``` ### Source Code (repository root) Likely existing runtime files: ```text apps/platform/app/Services/TenantConfiguration/GenericContentEvidenceCaptureService.php apps/platform/app/Services/TenantConfiguration/CoverageSourceContractResolver.php apps/platform/app/Services/TenantConfiguration/CoverageSourceContractDecision.php apps/platform/app/Services/TenantConfiguration/CoverageResourceUpserter.php apps/platform/app/Services/TenantConfiguration/CoverageEvidenceWriter.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCommandContracts.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellProductionRunner.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellInvocationReadinessEvaluator.php apps/platform/app/Support/OpsUx/OperationSummaryKeys.php apps/platform/app/Support/OpsUx/SummaryCountsNormalizer.php apps/platform/config/tenantpilot.php ``` Likely new runtime files or repo-canonical equivalents: ```text apps/platform/app/Services/TenantConfiguration/ExchangePowerShellEvidenceCaptureAdapter.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellCaptureEligibilityGate.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellContentOnlyEvidenceGuard.php apps/platform/app/Services/TenantConfiguration/ExchangePowerShellIdentityEvidenceGate.php ``` Implemented tests: ```text apps/platform/tests/Feature/TenantConfiguration/Spec434ExchangeEvidenceCaptureAdapterTest.php ``` **Structure Decision**: Backend TenantConfiguration service slice. No UI, routes, migrations, jobs, schedules, listeners, or customer-output paths. ## Complexity Tracking | Violation | Why Needed | Simpler Alternative Rejected Because | | --- | --- | --- | | New internal Exchange adapter/guard classes | Security/evidence correctness boundary between Exchange runner output and Coverage v2 writer | Reusing Graph capture would require fake endpoints/outcomes; manual evidence writes would bypass existing scope/redaction/storage rules | | Limited internal outcome/failure labels | Tests and implementation report need distinct safe blockers for prerequisite, identity, empty collection, over-promotion, and sanitized local failure behavior | Generic failed/unknown outcomes would hide whether evidence was safely blocked before append | ## Phase 0: Preflight and Scope Lock - Capture branch, HEAD, dirty state, and active spec path. - Re-check Specs 429-433 as read-only context. - Confirm Spec 433 PASS WITH CONDITIONS has no merge-blocking adapter safety issue. - Confirm no existing `specs/434-*` package was overwritten. - Confirm no UI/routes/jobs/schedules/listeners/migrations/customer output/tenant-id scope. ## Phase 1: Adapter Boundary and Eligibility - Add Exchange adapter or repo-canonical equivalent. - Accept only `transportRule`, `remoteDomain`, and `inboundConnector`. - Require `tenant_configuration.capture`, same workspace/environment/provider scope, Spec 433 readiness, `ProviderCapabilityEvaluator` `exchange_powershell_invoke` status `Supported`, Spec 432 accepted `ExchangePowerShellInvocationResult`, and Spec 430 verified command contract. - Add Exchange eligibility representation without fake Graph endpoint or fake captured outcome. - Prove no `ProviderGateway` / generic Graph list route is used for Exchange. - Preserve existing `CaptureOutcome` values for persisted evidence outcomes and keep adapter-local outcome/failure codes limited to the Spec 434 allowlist. ## Phase 2: Identity Hard-Stop - Evaluate identity before evidence writer append. - Block identity conflict, missing stable external ID, unsupported identity, duplicate identity, and display-name-only identity. - Ensure blocked identity returns sanitized outcome and creates no evidence row. - Prove writer non-reachability through adapter code order plus zero resource/evidence database assertions. ## Phase 3: Content-Only Guard - Add a max coverage level guard for the Exchange adapter path. - Prevent `CoverageEvidenceWriter` or adjacent code from promoting Exchange adapter evidence beyond `content_backed`. - Prove no comparable/renderable/certified/restore-ready/customer-claimable state appears. ## Phase 4: Empty Collection and Redaction - Treat empty Exchange runner collections as zero-item execution outcomes only. - Do not create fake resource/evidence rows for empty collections. - Keep raw stdout/stderr, payloads, serialized objects, credential material, tokens, and provider response bodies out of OperationRun context/logs/summary counts. - Use existing evidence storage only for non-empty guarded fixture evidence. - Map blocked/failure paths through sanitized ProviderReasonCodes or `ext.*` reason codes and the Spec 434 adapter-local code allowlist only. ## Phase 5: Regression and Static Guards - Run Spec 434 focus tests. - Run selected regressions for Specs 415, 417, 419, 420, 426, 427, 430, 431, 432, 433, provider capability registry/evaluator, and generic capture. - Add static/no-product-surface tests proving no route, Filament, Livewire, resource view, job, schedule, listener, migration, customer-output, or `tenant_id` path is introduced. ## Phase 6: Implementation Report and Close-Out - Create `implementation-report.md`. - Record candidate gate, branch/HEAD, dirty state, files changed, prerequisite proof, OperationRun decision, adapter proof, provider-capability proof, accepted runner-result proof, outcome/failure-code proof, identity hard-stop proof, content-only guard proof, empty collection proof, generic capture regression proof, redaction proof, no-promotion proof, no-product-surface proof, tests run, browser N/A, Laravel/PHP/PostgreSQL/Pest compatibility notes, deployment impact, and deferred work. ## Risk Controls - Stop if implementation requires new persistence for empty collections. - Stop if implementation requires a new OperationRun type or start surface. - Stop if implementation requires UI/product surface or customer output. - Stop if live Exchange provider calls are needed to validate the slice. - Stop if Graph capture must be modified in a way that changes existing Graph behavior. ## Spec Readiness Result PASS for preparation. Implementation remains a separate step and must follow `tasks.md`.