Summary: add Spec 434 Exchange PowerShell evidence capture adapter and prerequisite/identity/content-only guards; cap Exchange evidence at content_backed while preserving Graph capture. Validation: php artisan test --filter=Spec434 --compact; ./vendor/bin/pint --dirty --test; git diff --cached --check. Product Surface: N/A - no rendered UI surface changed; no deployment impact. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #501
20 KiB
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:assetsnot 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,tenantpilotExchange 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.captureOperationRun 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 --compactcd apps/platform && ./vendor/bin/sail artisan test --filter='Spec433|Spec432|Spec431|Spec430|Spec415|Spec417|Spec419|Spec420|Spec426|Spec427|ProviderCapabilityRegistryTest|ProviderCapabilityEvaluationTest' --compactcd apps/platform && ./vendor/bin/pint --dirty --testgit 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)
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:
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:
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:
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, andinboundConnector. - Require
tenant_configuration.capture, same workspace/environment/provider scope, Spec 433 readiness,ProviderCapabilityEvaluatorexchange_powershell_invokestatusSupported, Spec 432 acceptedExchangePowerShellInvocationResult, 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
CaptureOutcomevalues 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
CoverageEvidenceWriteror adjacent code from promoting Exchange adapter evidence beyondcontent_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_idpath 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.