Compare commits
2 Commits
dev
...
237-provid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62cc3a5f1f | ||
|
|
079a7dcaf3 |
4
.github/agents/copilot-instructions.md
vendored
4
.github/agents/copilot-instructions.md
vendored
@ -250,6 +250,8 @@ ## Active Technologies
|
|||||||
- Existing PostgreSQL tables only; no new table or schema migration is planned in the mainline slice (235-baseline-capture-truth)
|
- Existing PostgreSQL tables only; no new table or schema migration is planned in the mainline slice (235-baseline-capture-truth)
|
||||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing governance domain models and builders, existing Evidence Snapshot and Tenant Review infrastructure (236-canonical-control-catalog-foundation)
|
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing governance domain models and builders, existing Evidence Snapshot and Tenant Review infrastructure (236-canonical-control-catalog-foundation)
|
||||||
- PostgreSQL for existing downstream governance artifacts plus a product-seeded in-repo canonical control registry; no new DB-backed control authoring table in the first slice (236-canonical-control-catalog-foundation)
|
- PostgreSQL for existing downstream governance artifacts plus a product-seeded in-repo canonical control registry; no new DB-backed control authoring table in the first slice (236-canonical-control-catalog-foundation)
|
||||||
|
- PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + existing provider seams under `App\Services\Providers` and `App\Services\Graph`, especially `ProviderGateway`, `ProviderIdentityResolver`, `ProviderIdentityResolution`, `PlatformProviderIdentityResolver`, `ProviderConnectionResolver`, `ProviderConnectionResolution`, `MicrosoftGraphOptionsResolver`, `ProviderOperationRegistry`, `ProviderOperationStartGate`, `GraphClientInterface`, Pest v4 (237-provider-boundary-hardening)
|
||||||
|
- Existing PostgreSQL tables such as `provider_connections` and `operation_runs`; one new in-repo config catalog for provider-boundary ownership; no new database tables (237-provider-boundary-hardening)
|
||||||
|
|
||||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -284,9 +286,9 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 237-provider-boundary-hardening: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + existing provider seams under `App\Services\Providers` and `App\Services\Graph`, especially `ProviderGateway`, `ProviderIdentityResolver`, `ProviderIdentityResolution`, `PlatformProviderIdentityResolver`, `ProviderConnectionResolver`, `ProviderConnectionResolution`, `MicrosoftGraphOptionsResolver`, `ProviderOperationRegistry`, `ProviderOperationStartGate`, `GraphClientInterface`, Pest v4
|
||||||
- 236-canonical-control-catalog-foundation: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing governance domain models and builders, existing Evidence Snapshot and Tenant Review infrastructure
|
- 236-canonical-control-catalog-foundation: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing governance domain models and builders, existing Evidence Snapshot and Tenant Review infrastructure
|
||||||
- 235-baseline-capture-truth: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + `BaselineCaptureService`, `CaptureBaselineSnapshotJob`, `BaselineReasonCodes`, `BaselineCompareStats`, `ReasonTranslator`, `GovernanceRunDiagnosticSummaryBuilder`, `OperationRunService`, `BaselineProfile`, `BaselineSnapshot`, `OperationRunOutcome`, existing Filament capture/compare surfaces
|
- 235-baseline-capture-truth: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + `BaselineCaptureService`, `CaptureBaselineSnapshotJob`, `BaselineReasonCodes`, `BaselineCompareStats`, `ReasonTranslator`, `GovernanceRunDiagnosticSummaryBuilder`, `OperationRunService`, `BaselineProfile`, `BaselineSnapshot`, `OperationRunOutcome`, existing Filament capture/compare surfaces
|
||||||
- 234-dead-transitional-residue: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + `App\Models\BaselineProfile`, `App\Support\Baselines\BaselineProfileStatus`, `App\Support\Badges\BadgeCatalog`, `App\Support\Badges\BadgeDomain`, `Database\Factories\TenantFactory`, `App\Console\Commands\SeedBackupHealthBrowserFixture`, existing tenant-truth and baseline-profile Pest tests
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|
||||||
### Pre-production compatibility check
|
### Pre-production compatibility check
|
||||||
|
|||||||
@ -1,28 +1,30 @@
|
|||||||
<!--
|
<!--
|
||||||
Sync Impact Report
|
Sync Impact Report
|
||||||
|
|
||||||
- Version change: 2.8.0 -> 2.9.0
|
- Version change: 2.9.0 -> 2.10.0
|
||||||
- Modified principles:
|
- Modified principles:
|
||||||
- Added provider-boundary guardrail set under First Provider Is Not
|
- Expanded Operations / Run Observability Standard so OperationRun
|
||||||
Platform Core (PROV-001 with sub-rules PROV-002 through PROV-005)
|
start UX is shared-contract-owned instead of surface-owned
|
||||||
- Expanded Governance review expectations for provider-owned vs
|
- Expanded Governance review expectations for OperationRun-starting
|
||||||
platform-core boundaries
|
features, explicit queued-notification policy, and bounded
|
||||||
|
exceptions
|
||||||
- Added sections:
|
- Added sections:
|
||||||
- First Provider Is Not Platform Core (PROV-001): keeps Microsoft as
|
- OperationRun Start UX Contract (OPS-UX-START-001): centralizes
|
||||||
the current first provider without allowing provider-specific
|
queued toast/link/event/message semantics, run/artifact deep links,
|
||||||
semantics to silently become platform-core truth; requires explicit
|
queued DB-notification policy, and tenant/workspace-safe operation
|
||||||
review of provider-owned vs platform-core seams and prefers bounded
|
URL resolution behind one shared OperationRun UX layer
|
||||||
extraction over speculative multi-provider frameworks
|
|
||||||
- Removed sections: None
|
- Removed sections: None
|
||||||
- Templates requiring updates:
|
- Templates requiring updates:
|
||||||
- .specify/templates/spec-template.md: add provider-boundary platform
|
- .specify/templates/spec-template.md: add OperationRun UX Impact
|
||||||
core check ✅
|
section + start-contract prompts ✅
|
||||||
- .specify/templates/plan-template.md: add provider-boundary planning
|
- .specify/templates/plan-template.md: add OperationRun UX Impact
|
||||||
fields + constitution check ✅
|
planning section + constitution checks ✅
|
||||||
- .specify/templates/tasks-template.md: add provider-boundary task
|
- .specify/templates/tasks-template.md: add central start-UX reuse,
|
||||||
requirements ✅
|
queued-notification policy, and exception tasks ✅
|
||||||
- .specify/templates/checklist-template.md: add provider-boundary
|
- .specify/templates/checklist-template.md: add OperationRun start
|
||||||
review checks ✅
|
UX review checks ✅
|
||||||
|
- docs/product/standards/README.md: refresh constitution index for
|
||||||
|
the new ops-UX contract ✅
|
||||||
- Commands checked:
|
- Commands checked:
|
||||||
- N/A `.specify/templates/commands/*.md` directory is not present
|
- N/A `.specify/templates/commands/*.md` directory is not present
|
||||||
- Follow-up TODOs: None
|
- Follow-up TODOs: None
|
||||||
@ -307,24 +309,57 @@ ### Operations / Run Observability Standard
|
|||||||
even if implemented by multiple jobs/steps (“umbrella run”).
|
even if implemented by multiple jobs/steps (“umbrella run”).
|
||||||
- “Single-row” runs MUST still use consistent counters (e.g., `total=1`, `processed=0|1`) and outcome derived from success/failure.
|
- “Single-row” runs MUST still use consistent counters (e.g., `total=1`, `processed=0|1`) and outcome derived from success/failure.
|
||||||
- Monitoring pages MUST be DB-only at render time (no external calls).
|
- Monitoring pages MUST be DB-only at render time (no external calls).
|
||||||
- Start surfaces MUST NOT perform remote work inline; they only: authorize, create/reuse run (dedupe), enqueue work,
|
- Start surfaces MUST NOT perform remote work inline and MUST NOT compose OperationRun start UX locally; they only:
|
||||||
confirm + “View run”.
|
authorize, create/reuse run (dedupe), enqueue work, and hand queued/start-state feedback to the shared
|
||||||
|
OperationRun Start UX Contract.
|
||||||
|
|
||||||
|
### OperationRun Start UX Contract (OPS-UX-START-001)
|
||||||
|
|
||||||
|
- OperationRun UX MUST be contract-driven, not surface-driven.
|
||||||
|
- Any feature that creates, queues, deduplicates, resumes, blocks, completes, or links to an `OperationRun` MUST use
|
||||||
|
the central OperationRun Start UX Contract.
|
||||||
|
- Filament Pages, Resources, Widgets, Livewire Components, Actions, and Services MUST NOT independently compose
|
||||||
|
OperationRun start UX from local pieces.
|
||||||
|
- The shared OperationRun UX layer MUST own:
|
||||||
|
- local start notification / toast
|
||||||
|
- `Open operation` / `View run` link
|
||||||
|
- artifact link such as `View snapshot`, `View pack`, or `View restore`
|
||||||
|
- run-enqueued browser event
|
||||||
|
- queued DB-notification decision
|
||||||
|
- dedupe / already-available / already-running messaging
|
||||||
|
- blocked / failed-to-start messaging
|
||||||
|
- tenant/workspace-safe operation URL resolution
|
||||||
|
- Feature surfaces MAY initiate `OperationRun`s, but they MUST NOT define their own OperationRun UX semantics.
|
||||||
|
- `OperationRun` lifecycle state remains the canonical execution truth.
|
||||||
|
- Queued DB notifications MUST remain explicit opt-in unless the active spec defines a different policy.
|
||||||
|
- Terminal `OperationRun` notifications MUST be emitted through the central OperationRun lifecycle mechanism.
|
||||||
|
- Any exception MUST include:
|
||||||
|
1. an explicit spec decision,
|
||||||
|
2. a documented architecture note,
|
||||||
|
3. a test or guard-test exception with rationale,
|
||||||
|
4. a follow-up migration decision if the exception is temporary.
|
||||||
|
- New OperationRun-starting features MUST include an `OperationRun UX Impact` section in the active spec or plan.
|
||||||
|
|
||||||
### Operations UX — 3-Surface Feedback (OPS-UX-055) (NON-NEGOTIABLE)
|
### Operations UX — 3-Surface Feedback (OPS-UX-055) (NON-NEGOTIABLE)
|
||||||
|
|
||||||
If a feature creates/reuses `OperationRun`, it MUST use exactly three feedback surfaces — no others:
|
If a feature creates/reuses `OperationRun`, its default feedback contract is exactly three surfaces.
|
||||||
|
Queued DB notifications are forbidden by default and MAY exist only when the active spec explicitly opts into them
|
||||||
|
through the OperationRun Start UX Contract:
|
||||||
|
|
||||||
1) Toast (intent only / queued-only)
|
1) Toast (intent only / queued-only)
|
||||||
- A toast MAY be shown only when the run is accepted/queued (intent feedback).
|
- A toast MAY be shown only when the run is accepted/queued (intent feedback).
|
||||||
- The toast MUST use `OperationUxPresenter::queuedToast($operationType)->send()`.
|
- The toast MUST use `OperationUxPresenter::queuedToast($operationType)->send()`.
|
||||||
- Feature code MUST NOT craft ad-hoc operation toasts.
|
- Feature code MUST NOT craft ad-hoc operation toasts.
|
||||||
- A dedicated dedupe message MUST use the presenter (e.g., `alreadyQueuedToast(...)`), not `Notification::make()`.
|
- A dedicated dedupe message MUST use the presenter (e.g., `alreadyQueuedToast(...)`), not `Notification::make()`.
|
||||||
|
- Queued toast copy, action links, artifact links, start-state browser events, and dedupe/start-failure messaging MUST be
|
||||||
|
produced by the shared OperationRun Start UX Contract, not by local surface code.
|
||||||
|
|
||||||
2) Progress (active awareness only)
|
2) Progress (active awareness only)
|
||||||
- Live progress MUST exist only in:
|
- Live progress MUST exist only in:
|
||||||
- the global active-ops widget, and
|
- the global active-ops widget, and
|
||||||
- Monitoring → Operation Run Detail.
|
- Monitoring → Operation Run Detail.
|
||||||
- These surfaces MUST show only active runs (`queued|running`) and MUST never show terminal runs.
|
- These surfaces MUST show only active runs (`queued|running`) and MUST never show terminal runs.
|
||||||
|
- Running DB notifications are forbidden.
|
||||||
- Determinate progress is allowed ONLY when `summary_counts.total` and `summary_counts.processed` are valid numeric values.
|
- Determinate progress is allowed ONLY when `summary_counts.total` and `summary_counts.processed` are valid numeric values.
|
||||||
- Determinate progress MUST be clamped to 0–100. Otherwise render indeterminate + elapsed time.
|
- Determinate progress MUST be clamped to 0–100. Otherwise render indeterminate + elapsed time.
|
||||||
- The widget MUST NOT show percentage text (optional `processed/total` is allowed).
|
- The widget MUST NOT show percentage text (optional `processed/total` is allowed).
|
||||||
@ -365,6 +400,10 @@ ### Ops-UX regression guards are mandatory (OPS-UX-GUARD-001)
|
|||||||
|
|
||||||
The repo MUST include automated guards (Pest) that fail CI if:
|
The repo MUST include automated guards (Pest) that fail CI if:
|
||||||
- any direct `OperationRun` status/outcome transition occurs outside `OperationRunService`,
|
- any direct `OperationRun` status/outcome transition occurs outside `OperationRunService`,
|
||||||
|
- feature code bypasses the central OperationRun Start UX Contract for queued/start-state operation UX where the repo's
|
||||||
|
guardable patterns can detect it,
|
||||||
|
- feature code emits queued DB notifications for operations without explicit spec-driven opt-in through the shared
|
||||||
|
OperationRun UX layer,
|
||||||
- jobs emit DB notifications for operation completion/abort (`OperationRunCompleted` is the single terminal notification),
|
- jobs emit DB notifications for operation completion/abort (`OperationRunCompleted` is the single terminal notification),
|
||||||
- deprecated legacy operation notification classes are referenced again.
|
- deprecated legacy operation notification classes are referenced again.
|
||||||
|
|
||||||
@ -1614,6 +1653,11 @@ ### Scope, Compliance, and Review Expectations
|
|||||||
- Runtime-changing or test-affecting specs and PRs MUST include testing/lane/runtime impact covering actual test-purpose classification, affected lanes, fixture/helper/factory/seed/context cost changes, any heavy-family expansion, expected budget/baseline/trend effect, escalation decisions, and the minimal validation commands.
|
- Runtime-changing or test-affecting specs and PRs MUST include testing/lane/runtime impact covering actual test-purpose classification, affected lanes, fixture/helper/factory/seed/context cost changes, any heavy-family expansion, expected budget/baseline/trend effect, escalation decisions, and the minimal validation commands.
|
||||||
- Specs, plans, task lists, and review checklists MUST surface the test-governance questions needed to catch lane drift, hidden defaults, and runtime-cost escalation before merge.
|
- Specs, plans, task lists, and review checklists MUST surface the test-governance questions needed to catch lane drift, hidden defaults, and runtime-cost escalation before merge.
|
||||||
- Specs and PRs that touch shared provider/platform seams MUST classify the touched boundary as provider-owned or platform-core, keep provider-specific semantics out of platform-core contracts and vocabulary unless explicitly justified, and record whether any remaining hotspot is resolved in-feature or escalated as a follow-up spec.
|
- Specs and PRs that touch shared provider/platform seams MUST classify the touched boundary as provider-owned or platform-core, keep provider-specific semantics out of platform-core contracts and vocabulary unless explicitly justified, and record whether any remaining hotspot is resolved in-feature or escalated as a follow-up spec.
|
||||||
|
- Specs and PRs that create, queue, deduplicate, resume, block, complete, or deep-link to an `OperationRun` MUST reuse the
|
||||||
|
central OperationRun Start UX Contract, keep queued DB notifications explicit opt-in unless the active spec states a
|
||||||
|
different policy, route terminal notifications through the lifecycle mechanism, include an `OperationRun UX Impact`
|
||||||
|
section in the active spec or plan, and document any temporary exception with an architecture note, test rationale,
|
||||||
|
and migration decision.
|
||||||
- Specs and PRs that change operator-facing surfaces MUST classify each
|
- Specs and PRs that change operator-facing surfaces MUST classify each
|
||||||
affected surface under DECIDE-001 and justify any new Primary
|
affected surface under DECIDE-001 and justify any new Primary
|
||||||
Decision Surface or workflow-first navigation change.
|
Decision Surface or workflow-first navigation change.
|
||||||
@ -1631,4 +1675,4 @@ ### Versioning Policy (SemVer)
|
|||||||
- **MINOR**: new principle/section or materially expanded guidance.
|
- **MINOR**: new principle/section or materially expanded guidance.
|
||||||
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
- **MAJOR**: removing/redefining principles in a backward-incompatible way.
|
||||||
|
|
||||||
**Version**: 2.9.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-23
|
**Version**: 2.10.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-24
|
||||||
|
|||||||
@ -32,6 +32,13 @@ ## Shared Pattern Reuse
|
|||||||
- [ ] CHK008 The change extends the shared path where it is sufficient, or the deviation is explicitly documented with product reason, preserved consistency, ownership cost, and spread-control.
|
- [ ] CHK008 The change extends the shared path where it is sufficient, or the deviation is explicitly documented with product reason, preserved consistency, ownership cost, and spread-control.
|
||||||
- [ ] CHK009 The change does not create a parallel operator-facing UX language for the same interaction class unless a bounded exception is recorded.
|
- [ ] CHK009 The change does not create a parallel operator-facing UX language for the same interaction class unless a bounded exception is recorded.
|
||||||
|
|
||||||
|
## OperationRun Start UX Contract
|
||||||
|
|
||||||
|
- [ ] CHK019 The change explicitly says whether it creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`, and the required `OperationRun UX Impact` section exists when applicable.
|
||||||
|
- [ ] CHK020 Queued toast/link/artifact-link/browser-event/dedupe-or-blocked messaging and tenant/workspace-safe operation URL resolution are delegated to the shared OperationRun UX contract instead of local surface code.
|
||||||
|
- [ ] CHK021 Any queued DB notification is explicit opt-in in the active spec or plan, running DB notifications remain absent, and terminal notifications still flow through the central lifecycle mechanism.
|
||||||
|
- [ ] CHK022 Any exception records the explicit spec decision, architecture note, test or guard-test rationale, and temporary migration follow-up decision.
|
||||||
|
|
||||||
## Provider Boundary And Vocabulary
|
## Provider Boundary And Vocabulary
|
||||||
|
|
||||||
- [ ] CHK010 The change states whether any touched shared seam is provider-owned, platform-core, or mixed, and provider-specific semantics do not silently spread into platform-core contracts, taxonomy, identifiers, compare semantics, or operator vocabulary.
|
- [ ] CHK010 The change states whether any touched shared seam is provider-owned, platform-core, or mixed, and provider-specific semantics do not silently spread into platform-core contracts, taxonomy, identifiers, compare semantics, or operator vocabulary.
|
||||||
|
|||||||
@ -54,6 +54,18 @@ ## Shared Pattern & System Fit
|
|||||||
- **Why the existing abstraction was sufficient or insufficient**: [Short explanation tied to current-release truth]
|
- **Why the existing abstraction was sufficient or insufficient**: [Short explanation tied to current-release truth]
|
||||||
- **Bounded deviation / spread control**: [none / describe the exception boundary and containment rule]
|
- **Bounded deviation / spread control**: [none / describe the exception boundary and containment rule]
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
> **Fill when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`. Docs-only or template-only work may use concise `N/A`.**
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: [yes / no / N/A]
|
||||||
|
- **Central contract reused**: [shared OperationRun UX layer / `N/A`]
|
||||||
|
- **Delegated UX behaviors**: [queued toast / run link / artifact link / run-enqueued browser event / queued DB-notification decision / dedupe-or-blocked messaging / tenant/workspace-safe URL resolution / `N/A`]
|
||||||
|
- **Surface-owned behavior kept local**: [initiation inputs only / none / short explanation]
|
||||||
|
- **Queued DB-notification policy**: [explicit opt-in / spec override / `N/A`]
|
||||||
|
- **Terminal notification path**: [central lifecycle mechanism / `N/A`]
|
||||||
|
- **Exception path**: [none / spec decision + architecture note + test rationale + temporary migration follow-up]
|
||||||
|
|
||||||
## Provider Boundary & Portability Fit
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
> **Fill when the feature touches shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth. Docs-only or template-only work may use concise `N/A`.**
|
> **Fill when the feature touches shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth. Docs-only or template-only work may use concise `N/A`.**
|
||||||
@ -79,7 +91,8 @@ ## Constitution Check
|
|||||||
- RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
|
- RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
|
||||||
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
|
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
|
||||||
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on `/auth/*` without `OperationRun`
|
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on `/auth/*` without `OperationRun`
|
||||||
- Ops-UX 3-surface feedback: if `OperationRun` is used, feedback is exactly toast intent-only + progress surfaces + exactly-once terminal `OperationRunCompleted` (initiator-only); no queued/running DB notifications
|
- OperationRun start UX: any feature that creates, queues, deduplicates, resumes, blocks, completes, or links `OperationRun` reuses the central OperationRun Start UX Contract; no local composition of queued toast/link/event/start-state messaging; `OperationRun UX Impact` is present in the active spec or plan
|
||||||
|
- Ops-UX 3-surface feedback: if `OperationRun` is used, default feedback is toast intent-only + progress surfaces + exactly-once terminal `OperationRunCompleted` (initiator-only); queued DB notifications remain explicit opt-in through the shared start UX contract; running DB notifications stay disallowed
|
||||||
- Ops-UX lifecycle: `OperationRun.status` / `OperationRun.outcome` transitions are service-owned (only via `OperationRunService`); context-only updates allowed outside
|
- Ops-UX lifecycle: `OperationRun.status` / `OperationRun.outcome` transitions are service-owned (only via `OperationRunService`); context-only updates allowed outside
|
||||||
- Ops-UX summary counts: `summary_counts` keys come from `OperationSummaryKeys::all()` and values are flat numeric-only
|
- Ops-UX summary counts: `summary_counts` keys come from `OperationSummaryKeys::all()` and values are flat numeric-only
|
||||||
- Ops-UX guards: CI has regression guards that fail with actionable output (file + snippet) when these patterns regress
|
- Ops-UX guards: CI has regression guards that fail with actionable output (file + snippet) when these patterns regress
|
||||||
|
|||||||
@ -47,6 +47,16 @@ ## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches not
|
|||||||
- **Consistency impact**: [What must stay aligned across interaction structure, copy, status semantics, actions, and deep links]
|
- **Consistency impact**: [What must stay aligned across interaction structure, copy, status semantics, actions, and deep links]
|
||||||
- **Review focus**: [What reviewers must verify to prevent parallel local patterns]
|
- **Review focus**: [What reviewers must verify to prevent parallel local patterns]
|
||||||
|
|
||||||
|
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: [yes/no]
|
||||||
|
- **Shared OperationRun UX contract/layer reused**: [Name it or `N/A`]
|
||||||
|
- **Delegated start/completion UX behaviors**: [queued toast / `Open operation` or `View run` link / artifact link / run-enqueued browser event / queued DB-notification decision / dedupe-or-blocked messaging / tenant/workspace-safe URL resolution / `N/A`]
|
||||||
|
- **Local surface-owned behavior that remains**: [initiation inputs only / none / bounded explanation]
|
||||||
|
- **Queued DB-notification policy**: [explicit opt-in / spec override / `N/A`]
|
||||||
|
- **Terminal notification path**: [central lifecycle mechanism / `N/A`]
|
||||||
|
- **Exception required?**: [none / explicit spec decision + architecture note + test or guard-test rationale + temporary migration follow-up]
|
||||||
|
|
||||||
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||||
|
|
||||||
- **Shared provider/platform boundary touched?**: [yes/no]
|
- **Shared provider/platform boundary touched?**: [yes/no]
|
||||||
@ -263,12 +273,21 @@ ## Requirements *(mandatory)*
|
|||||||
- and the exact minimal validation commands reviewers should run.
|
- and the exact minimal validation commands reviewers should run.
|
||||||
|
|
||||||
**Constitution alignment (OPS-UX):** If this feature creates/reuses an `OperationRun`, the spec MUST:
|
**Constitution alignment (OPS-UX):** If this feature creates/reuses an `OperationRun`, the spec MUST:
|
||||||
- explicitly state compliance with the Ops-UX 3-surface feedback contract (toast intent-only, progress surfaces, terminal DB notification),
|
- explicitly state compliance with the default Ops-UX 3-surface feedback contract (toast intent-only, progress surfaces, terminal DB notification) and whether any queued DB notification is explicitly opted into,
|
||||||
- state that `OperationRun.status` / `OperationRun.outcome` transitions are service-owned (only via `OperationRunService`),
|
- state that `OperationRun.status` / `OperationRun.outcome` transitions are service-owned (only via `OperationRunService`),
|
||||||
- describe how `summary_counts` keys/values comply with `OperationSummaryKeys::all()` and numeric-only rules,
|
- describe how `summary_counts` keys/values comply with `OperationSummaryKeys::all()` and numeric-only rules,
|
||||||
- clarify scheduled/system-run behavior (initiator null → no terminal DB notification; audit is via Monitoring),
|
- clarify scheduled/system-run behavior (initiator null → no terminal DB notification; audit is via Monitoring),
|
||||||
- list which regression guard tests are added/updated to keep these rules enforceable in CI.
|
- list which regression guard tests are added/updated to keep these rules enforceable in CI.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-UX-START-001):** If this feature creates, queues, deduplicates, resumes, blocks, completes, or links to an `OperationRun`, the spec MUST:
|
||||||
|
- include the `OperationRun UX Impact` section,
|
||||||
|
- name the shared OperationRun UX contract/layer being reused,
|
||||||
|
- delegate queued toast/link/artifact-link/browser-event/queued-DB-notification/dedupe-or-blocked messaging/tenant-safe URL resolution to that shared path,
|
||||||
|
- keep local surface code limited to initiation inputs and operation-specific data capture,
|
||||||
|
- keep queued DB notifications explicit opt-in unless the spec intentionally defines a different policy,
|
||||||
|
- route terminal notifications through the central lifecycle mechanism,
|
||||||
|
- and document any exception with an explicit spec decision, architecture note, test or guard-test rationale, and temporary follow-up migration decision.
|
||||||
|
|
||||||
**Constitution alignment (RBAC-UX):** If this feature introduces or changes authorization behavior, the spec MUST:
|
**Constitution alignment (RBAC-UX):** If this feature introduces or changes authorization behavior, the spec MUST:
|
||||||
- state which authorization plane(s) are involved (tenant/admin `/admin` + tenant-context `/admin/t/{tenant}/...` vs platform `/system`),
|
- state which authorization plane(s) are involved (tenant/admin `/admin` + tenant-context `/admin/t/{tenant}/...` vs platform `/system`),
|
||||||
- ensure any cross-plane access is deny-as-not-found (404),
|
- ensure any cross-plane access is deny-as-not-found (404),
|
||||||
|
|||||||
@ -18,17 +18,22 @@ # Tasks: [FEATURE NAME]
|
|||||||
- record budget, baseline, or trend follow-up when runtime cost shifts materially,
|
- record budget, baseline, or trend follow-up when runtime cost shifts materially,
|
||||||
- and document whether the change resolves as `document-in-feature`, `follow-up-spec`, or `reject-or-split`.
|
- and document whether the change resolves as `document-in-feature`, `follow-up-spec`, or `reject-or-split`.
|
||||||
**Operations**: If this feature introduces long-running/remote/queued/scheduled work, include tasks to create/reuse and update a
|
**Operations**: If this feature introduces long-running/remote/queued/scheduled work, include tasks to create/reuse and update a
|
||||||
canonical `OperationRun`, and ensure “View run” links route to the canonical Monitoring hub.
|
canonical `OperationRun`, and ensure “View run” links route to the canonical Monitoring hub through the shared OperationRun start UX path rather than local surface composition.
|
||||||
If security-relevant DB-only actions skip `OperationRun`, include tasks for `AuditLog` entries (before/after + actor + tenant).
|
If security-relevant DB-only actions skip `OperationRun`, include tasks for `AuditLog` entries (before/after + actor + tenant).
|
||||||
Auth handshake exception (OPS-EX-AUTH-001): OIDC/SAML login handshakes may perform synchronous outbound HTTP on `/auth/*` endpoints
|
Auth handshake exception (OPS-EX-AUTH-001): OIDC/SAML login handshakes may perform synchronous outbound HTTP on `/auth/*` endpoints
|
||||||
without an `OperationRun`.
|
without an `OperationRun`.
|
||||||
If this feature creates/reuses an `OperationRun`, tasks MUST also include:
|
If this feature creates/reuses an `OperationRun`, tasks MUST also include:
|
||||||
|
- reusing the central OperationRun Start UX Contract instead of composing local queued toast/link/event/dedupe/blocked/start-failure semantics,
|
||||||
|
- delegating `Open operation` / `View run`, artifact links, run-enqueued browser event, queued DB-notification policy, dedupe / already-available / already-running messaging, blocked / failed-to-start messaging, and tenant/workspace-safe URL resolution to the shared OperationRun UX layer,
|
||||||
- enforcing the Ops-UX 3-surface feedback contract (toast intent-only via `OperationUxPresenter`, progress only in widget + run detail, terminal notification is `OperationRunCompleted` exactly-once, initiator-only),
|
- enforcing the Ops-UX 3-surface feedback contract (toast intent-only via `OperationUxPresenter`, progress only in widget + run detail, terminal notification is `OperationRunCompleted` exactly-once, initiator-only),
|
||||||
- ensuring no queued/running DB notifications exist anywhere for operations (no `sendToDatabase()` for queued/running/completion/abort in feature code),
|
- keeping queued DB notifications explicit opt-in in the active spec unless a different policy is intentionally approved, and ensuring running DB notifications do not exist,
|
||||||
|
- routing terminal notifications through the central lifecycle mechanism rather than feature-local notification code,
|
||||||
- ensuring `OperationRun.status` / `OperationRun.outcome` transitions happen only via `OperationRunService`,
|
- ensuring `OperationRun.status` / `OperationRun.outcome` transitions happen only via `OperationRunService`,
|
||||||
- ensuring `summary_counts` keys come from `OperationSummaryKeys::all()` and values are flat numeric-only,
|
- ensuring `summary_counts` keys come from `OperationSummaryKeys::all()` and values are flat numeric-only,
|
||||||
- adding/updating Ops-UX regression guards (Pest) that fail CI with actionable output (file + snippet) when these patterns regress,
|
- adding/updating Ops-UX regression guards (Pest) that fail CI with actionable output (file + snippet) when these patterns regress,
|
||||||
- clarifying scheduled/system-run behavior (initiator null → no terminal DB notification; audit via Monitoring; tenant-wide alerting via Alerts system).
|
- clarifying scheduled/system-run behavior (initiator null → no terminal DB notification; audit via Monitoring; tenant-wide alerting via Alerts system),
|
||||||
|
- documenting any exception with an explicit spec decision, architecture note, test or guard-test rationale, and temporary migration follow-up decision,
|
||||||
|
- and ensuring the active spec or plan contains an `OperationRun UX Impact` section.
|
||||||
**RBAC**: If this feature introduces or changes authorization, tasks MUST include:
|
**RBAC**: If this feature introduces or changes authorization, tasks MUST include:
|
||||||
- explicit Gate/Policy enforcement for all mutation endpoints/actions,
|
- explicit Gate/Policy enforcement for all mutation endpoints/actions,
|
||||||
- explicit 404 vs 403 semantics:
|
- explicit 404 vs 403 semantics:
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
use App\Models\ProviderConnection;
|
use App\Models\ProviderConnection;
|
||||||
use App\Services\Graph\GraphClientInterface;
|
use App\Services\Graph\GraphClientInterface;
|
||||||
use App\Services\Graph\GraphResponse;
|
use App\Services\Graph\GraphResponse;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
final class ProviderGateway
|
final class ProviderGateway
|
||||||
{
|
{
|
||||||
@ -53,6 +55,17 @@ public function request(ProviderConnection $connection, string $method, string $
|
|||||||
*/
|
*/
|
||||||
public function graphOptions(ProviderConnection $connection, array $overrides = []): array
|
public function graphOptions(ProviderConnection $connection, array $overrides = []): array
|
||||||
{
|
{
|
||||||
return $this->identityResolver->resolve($connection)->graphOptions($overrides);
|
$resolution = $this->identityResolver->resolve($connection);
|
||||||
|
|
||||||
|
if (! $resolution->resolved || $resolution->effectiveClientId === null || $resolution->clientSecret === null) {
|
||||||
|
throw new RuntimeException($resolution->message ?? 'Provider identity could not be resolved.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_merge([
|
||||||
|
'tenant' => $resolution->tenantContext,
|
||||||
|
'client_id' => $resolution->effectiveClientId,
|
||||||
|
'client_secret' => $resolution->clientSecret,
|
||||||
|
'client_request_id' => (string) Str::uuid(),
|
||||||
|
], $overrides);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
use App\Support\Providers\ProviderConnectionType;
|
use App\Support\Providers\ProviderConnectionType;
|
||||||
use App\Support\Providers\ProviderReasonCodes;
|
use App\Support\Providers\ProviderReasonCodes;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
final class ProviderIdentityResolution
|
final class ProviderIdentityResolution
|
||||||
{
|
{
|
||||||
@ -66,24 +64,6 @@ public static function blocked(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $overrides
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function graphOptions(array $overrides = []): array
|
|
||||||
{
|
|
||||||
if (! $this->resolved || $this->effectiveClientId === null || $this->clientSecret === null) {
|
|
||||||
throw new RuntimeException($this->message ?? 'Provider identity could not be resolved.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_merge([
|
|
||||||
'tenant' => $this->tenantContext,
|
|
||||||
'client_id' => $this->effectiveClientId,
|
|
||||||
'client_secret' => $this->clientSecret,
|
|
||||||
'client_request_id' => (string) Str::uuid(),
|
|
||||||
], $overrides);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function effectiveReasonCode(): string
|
public function effectiveReasonCode(): string
|
||||||
{
|
{
|
||||||
return $this->reasonCode ?? ProviderReasonCodes::UnknownError;
|
return $this->reasonCode ?? ProviderReasonCodes::UnknownError;
|
||||||
|
|||||||
@ -7,44 +7,48 @@
|
|||||||
|
|
||||||
final class ProviderOperationRegistry
|
final class ProviderOperationRegistry
|
||||||
{
|
{
|
||||||
|
public const string BINDING_ACTIVE = 'active';
|
||||||
|
|
||||||
|
public const string BINDING_UNSUPPORTED = 'unsupported';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, array{provider: string, module: string, label: string, required_capability: string}>
|
* @return array<string, array{operation_type: string, module: string, label: string, required_capability: string}>
|
||||||
*/
|
*/
|
||||||
public function all(): array
|
public function definitions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'provider.connection.check' => [
|
'provider.connection.check' => [
|
||||||
'provider' => 'microsoft',
|
'operation_type' => 'provider.connection.check',
|
||||||
'module' => 'health_check',
|
'module' => 'health_check',
|
||||||
'label' => 'Provider connection check',
|
'label' => 'Provider connection check',
|
||||||
'required_capability' => Capabilities::PROVIDER_RUN,
|
'required_capability' => Capabilities::PROVIDER_RUN,
|
||||||
],
|
],
|
||||||
'inventory_sync' => [
|
'inventory_sync' => [
|
||||||
'provider' => 'microsoft',
|
'operation_type' => 'inventory_sync',
|
||||||
'module' => 'inventory',
|
'module' => 'inventory',
|
||||||
'label' => 'Inventory sync',
|
'label' => 'Inventory sync',
|
||||||
'required_capability' => Capabilities::PROVIDER_RUN,
|
'required_capability' => Capabilities::PROVIDER_RUN,
|
||||||
],
|
],
|
||||||
'compliance.snapshot' => [
|
'compliance.snapshot' => [
|
||||||
'provider' => 'microsoft',
|
'operation_type' => 'compliance.snapshot',
|
||||||
'module' => 'compliance',
|
'module' => 'compliance',
|
||||||
'label' => 'Compliance snapshot',
|
'label' => 'Compliance snapshot',
|
||||||
'required_capability' => Capabilities::PROVIDER_RUN,
|
'required_capability' => Capabilities::PROVIDER_RUN,
|
||||||
],
|
],
|
||||||
'restore.execute' => [
|
'restore.execute' => [
|
||||||
'provider' => 'microsoft',
|
'operation_type' => 'restore.execute',
|
||||||
'module' => 'restore',
|
'module' => 'restore',
|
||||||
'label' => 'Restore execution',
|
'label' => 'Restore execution',
|
||||||
'required_capability' => Capabilities::TENANT_MANAGE,
|
'required_capability' => Capabilities::TENANT_MANAGE,
|
||||||
],
|
],
|
||||||
'entra_group_sync' => [
|
'entra_group_sync' => [
|
||||||
'provider' => 'microsoft',
|
'operation_type' => 'entra_group_sync',
|
||||||
'module' => 'directory_groups',
|
'module' => 'directory_groups',
|
||||||
'label' => 'Directory groups sync',
|
'label' => 'Directory groups sync',
|
||||||
'required_capability' => Capabilities::TENANT_SYNC,
|
'required_capability' => Capabilities::TENANT_SYNC,
|
||||||
],
|
],
|
||||||
'directory_role_definitions.sync' => [
|
'directory_role_definitions.sync' => [
|
||||||
'provider' => 'microsoft',
|
'operation_type' => 'directory_role_definitions.sync',
|
||||||
'module' => 'directory_role_definitions',
|
'module' => 'directory_role_definitions',
|
||||||
'label' => 'Role definitions sync',
|
'label' => 'Role definitions sync',
|
||||||
'required_capability' => Capabilities::TENANT_MANAGE,
|
'required_capability' => Capabilities::TENANT_MANAGE,
|
||||||
@ -52,19 +56,78 @@ public function all(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isAllowed(string $operationType): bool
|
/**
|
||||||
|
* @return array<string, array{operation_type: string, module: string, label: string, required_capability: string}>
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
{
|
{
|
||||||
return array_key_exists($operationType, $this->all());
|
return $this->definitions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{provider: string, module: string, label: string, required_capability: string}
|
* @return array<string, array<string, array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}>>
|
||||||
|
*/
|
||||||
|
public function providerBindings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'provider.connection.check' => [
|
||||||
|
'microsoft' => $this->activeMicrosoftBinding(
|
||||||
|
operationType: 'provider.connection.check',
|
||||||
|
handlerNotes: 'Uses the current Microsoft Graph provider connection health-check workflow.',
|
||||||
|
exceptionNotes: 'Current-release provider binding remains Microsoft-only until a real second provider case exists.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'inventory_sync' => [
|
||||||
|
'microsoft' => $this->activeMicrosoftBinding(
|
||||||
|
operationType: 'inventory_sync',
|
||||||
|
handlerNotes: 'Uses the current Microsoft Intune inventory sync workflow.',
|
||||||
|
exceptionNotes: 'Inventory collection is currently Microsoft Intune-specific provider behavior.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'compliance.snapshot' => [
|
||||||
|
'microsoft' => $this->activeMicrosoftBinding(
|
||||||
|
operationType: 'compliance.snapshot',
|
||||||
|
handlerNotes: 'Uses the current Microsoft compliance snapshot workflow.',
|
||||||
|
exceptionNotes: 'Compliance snapshot runtime remains bounded to the Microsoft provider.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'restore.execute' => [
|
||||||
|
'microsoft' => $this->activeMicrosoftBinding(
|
||||||
|
operationType: 'restore.execute',
|
||||||
|
handlerNotes: 'Uses the current Microsoft restore execution workflow.',
|
||||||
|
exceptionNotes: 'Restore execution remains Microsoft-only and must preserve dry-run and audit safeguards.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'entra_group_sync' => [
|
||||||
|
'microsoft' => $this->activeMicrosoftBinding(
|
||||||
|
operationType: 'entra_group_sync',
|
||||||
|
handlerNotes: 'Uses the current Microsoft Entra group synchronization workflow.',
|
||||||
|
exceptionNotes: 'The operation type keeps current Entra vocabulary until the identity-neutrality follow-up.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'directory_role_definitions.sync' => [
|
||||||
|
'microsoft' => $this->activeMicrosoftBinding(
|
||||||
|
operationType: 'directory_role_definitions.sync',
|
||||||
|
handlerNotes: 'Uses the current Microsoft directory role definition synchronization workflow.',
|
||||||
|
exceptionNotes: 'Directory role definitions are Microsoft-owned provider semantics.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAllowed(string $operationType): bool
|
||||||
|
{
|
||||||
|
return array_key_exists(trim($operationType), $this->definitions());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{operation_type: string, module: string, label: string, required_capability: string}
|
||||||
*/
|
*/
|
||||||
public function get(string $operationType): array
|
public function get(string $operationType): array
|
||||||
{
|
{
|
||||||
$operationType = trim($operationType);
|
$operationType = trim($operationType);
|
||||||
|
|
||||||
$definition = $this->all()[$operationType] ?? null;
|
$definition = $this->definitions()[$operationType] ?? null;
|
||||||
|
|
||||||
if (! is_array($definition)) {
|
if (! is_array($definition)) {
|
||||||
throw new InvalidArgumentException("Unknown provider operation type: {$operationType}");
|
throw new InvalidArgumentException("Unknown provider operation type: {$operationType}");
|
||||||
@ -72,4 +135,85 @@ public function get(string $operationType): array
|
|||||||
|
|
||||||
return $definition;
|
return $definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}|null
|
||||||
|
*/
|
||||||
|
public function bindingFor(string $operationType, string $provider): ?array
|
||||||
|
{
|
||||||
|
$operationType = trim($operationType);
|
||||||
|
$provider = trim($provider);
|
||||||
|
|
||||||
|
if ($operationType === '' || $provider === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bindings = $this->providerBindings()[$operationType] ?? [];
|
||||||
|
|
||||||
|
return $bindings[$provider] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}|null
|
||||||
|
*/
|
||||||
|
public function activeBindingFor(string $operationType): ?array
|
||||||
|
{
|
||||||
|
$operationType = trim($operationType);
|
||||||
|
$bindings = $this->providerBindings()[$operationType] ?? [];
|
||||||
|
|
||||||
|
foreach ($bindings as $binding) {
|
||||||
|
if (($binding['binding_status'] ?? null) === self::BINDING_ACTIVE) {
|
||||||
|
return $binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* definition: array{operation_type: string, module: string, label: string, required_capability: string},
|
||||||
|
* binding: array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function boundaryOperation(string $operationType, ?string $provider = null): array
|
||||||
|
{
|
||||||
|
$definition = $this->get($operationType);
|
||||||
|
$binding = is_string($provider) && trim($provider) !== ''
|
||||||
|
? $this->bindingFor($operationType, $provider)
|
||||||
|
: $this->activeBindingFor($operationType);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'definition' => $definition,
|
||||||
|
'binding' => $binding ?? $this->unsupportedBinding($operationType, $provider ?? 'unknown'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}
|
||||||
|
*/
|
||||||
|
public function unsupportedBinding(string $operationType, string $provider): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'operation_type' => trim($operationType),
|
||||||
|
'provider' => trim($provider) !== '' ? trim($provider) : 'unknown',
|
||||||
|
'binding_status' => self::BINDING_UNSUPPORTED,
|
||||||
|
'handler_notes' => 'No explicit provider binding exists for this operation/provider combination.',
|
||||||
|
'exception_notes' => 'Unsupported combinations must block explicitly instead of inheriting Microsoft behavior.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}
|
||||||
|
*/
|
||||||
|
private function activeMicrosoftBinding(string $operationType, string $handlerNotes, string $exceptionNotes): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'operation_type' => $operationType,
|
||||||
|
'provider' => 'microsoft',
|
||||||
|
'binding_status' => self::BINDING_ACTIVE,
|
||||||
|
'handler_notes' => $handlerNotes,
|
||||||
|
'exception_notes' => $exceptionNotes,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,26 +42,47 @@ public function start(
|
|||||||
array $extraContext = [],
|
array $extraContext = [],
|
||||||
): ProviderOperationStartResult {
|
): ProviderOperationStartResult {
|
||||||
$definition = $this->registry->get($operationType);
|
$definition = $this->registry->get($operationType);
|
||||||
|
$binding = $this->resolveProviderBinding($operationType, $connection);
|
||||||
|
|
||||||
|
if (($binding['binding_status'] ?? null) !== ProviderOperationRegistry::BINDING_ACTIVE) {
|
||||||
|
return $this->startBlocked(
|
||||||
|
tenant: $tenant,
|
||||||
|
operationType: $operationType,
|
||||||
|
provider: (string) ($binding['provider'] ?? 'unknown'),
|
||||||
|
module: (string) $definition['module'],
|
||||||
|
reasonCode: ProviderReasonCodes::ProviderBindingUnsupported,
|
||||||
|
extensionReasonCode: 'ext.provider_binding_missing',
|
||||||
|
reasonMessage: 'No explicit provider binding supports this operation/provider combination.',
|
||||||
|
connection: $connection,
|
||||||
|
initiator: $initiator,
|
||||||
|
extraContext: array_merge($extraContext, [
|
||||||
|
'provider_binding' => $this->bindingContext($binding),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$resolution = $connection instanceof ProviderConnection
|
$resolution = $connection instanceof ProviderConnection
|
||||||
? $this->resolver->validateConnection($tenant, (string) $definition['provider'], $connection)
|
? $this->resolver->validateConnection($tenant, (string) $binding['provider'], $connection)
|
||||||
: $this->resolver->resolveDefault($tenant, (string) $definition['provider']);
|
: $this->resolver->resolveDefault($tenant, (string) $binding['provider']);
|
||||||
|
|
||||||
if (! $resolution->resolved || ! $resolution->connection instanceof ProviderConnection) {
|
if (! $resolution->resolved || ! $resolution->connection instanceof ProviderConnection) {
|
||||||
return $this->startBlocked(
|
return $this->startBlocked(
|
||||||
tenant: $tenant,
|
tenant: $tenant,
|
||||||
operationType: $operationType,
|
operationType: $operationType,
|
||||||
provider: (string) $definition['provider'],
|
provider: (string) $binding['provider'],
|
||||||
module: (string) $definition['module'],
|
module: (string) $definition['module'],
|
||||||
reasonCode: $resolution->effectiveReasonCode(),
|
reasonCode: $resolution->effectiveReasonCode(),
|
||||||
extensionReasonCode: $resolution->extensionReasonCode,
|
extensionReasonCode: $resolution->extensionReasonCode,
|
||||||
reasonMessage: $resolution->message,
|
reasonMessage: $resolution->message,
|
||||||
connection: $resolution->connection ?? $connection,
|
connection: $resolution->connection ?? $connection,
|
||||||
initiator: $initiator,
|
initiator: $initiator,
|
||||||
extraContext: $extraContext,
|
extraContext: array_merge($extraContext, [
|
||||||
|
'provider_binding' => $this->bindingContext($binding),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($tenant, $operationType, $dispatcher, $initiator, $extraContext, $definition, $resolution): ProviderOperationStartResult {
|
return DB::transaction(function () use ($tenant, $operationType, $dispatcher, $initiator, $extraContext, $definition, $binding, $resolution): ProviderOperationStartResult {
|
||||||
$connection = $resolution->connection;
|
$connection = $resolution->connection;
|
||||||
|
|
||||||
if (! $connection instanceof ProviderConnection) {
|
if (! $connection instanceof ProviderConnection) {
|
||||||
@ -114,6 +135,7 @@ public function start(
|
|||||||
'required_capability' => $this->resolveRequiredCapability($operationType, $extraContext),
|
'required_capability' => $this->resolveRequiredCapability($operationType, $extraContext),
|
||||||
'provider' => $lockedConnection->provider,
|
'provider' => $lockedConnection->provider,
|
||||||
'module' => $definition['module'],
|
'module' => $definition['module'],
|
||||||
|
'provider_binding' => $this->bindingContext($binding),
|
||||||
'provider_connection_id' => (int) $lockedConnection->getKey(),
|
'provider_connection_id' => (int) $lockedConnection->getKey(),
|
||||||
'target_scope' => [
|
'target_scope' => [
|
||||||
'entra_tenant_id' => $lockedConnection->entra_tenant_id,
|
'entra_tenant_id' => $lockedConnection->entra_tenant_id,
|
||||||
@ -235,6 +257,36 @@ private function invokeDispatcher(callable $dispatcher, OperationRun $run): void
|
|||||||
$dispatcher();
|
$dispatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string}
|
||||||
|
*/
|
||||||
|
private function resolveProviderBinding(string $operationType, ?ProviderConnection $connection): array
|
||||||
|
{
|
||||||
|
if ($connection instanceof ProviderConnection) {
|
||||||
|
$provider = trim((string) $connection->provider);
|
||||||
|
|
||||||
|
return $this->registry->bindingFor($operationType, $provider)
|
||||||
|
?? $this->registry->unsupportedBinding($operationType, $provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->registry->activeBindingFor($operationType)
|
||||||
|
?? $this->registry->unsupportedBinding($operationType, 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{operation_type: string, provider: string, binding_status: string, handler_notes: string, exception_notes: string} $binding
|
||||||
|
* @return array{provider: string, binding_status: string, handler_notes: string, exception_notes: string}
|
||||||
|
*/
|
||||||
|
private function bindingContext(array $binding): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'provider' => (string) $binding['provider'],
|
||||||
|
'binding_status' => (string) $binding['binding_status'],
|
||||||
|
'handler_notes' => (string) $binding['handler_notes'],
|
||||||
|
'exception_notes' => (string) $binding['exception_notes'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $extraContext
|
* @param array<string, mixed> $extraContext
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,194 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Providers\Boundary;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class ProviderBoundaryCatalog
|
||||||
|
{
|
||||||
|
public const string STATUS_ALLOWED = 'allowed';
|
||||||
|
|
||||||
|
public const string STATUS_REVIEW_REQUIRED = 'review_required';
|
||||||
|
|
||||||
|
public const string STATUS_BLOCKED = 'blocked';
|
||||||
|
|
||||||
|
public const string VIOLATION_NONE = 'none';
|
||||||
|
|
||||||
|
public const string VIOLATION_PLATFORM_CORE_PROVIDER_LEAK = 'platform_core_provider_leak';
|
||||||
|
|
||||||
|
public const string VIOLATION_UNDECLARED_EXCEPTION = 'undeclared_exception';
|
||||||
|
|
||||||
|
public const string VIOLATION_MISSING_PROVIDER_BINDING = 'missing_provider_binding';
|
||||||
|
|
||||||
|
public const string VIOLATION_PROVIDER_BINDING_AS_PRIMARY_TRUTH = 'provider_binding_as_primary_truth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, ProviderBoundarySeam>
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
$seams = config('provider_boundaries.seams', []);
|
||||||
|
|
||||||
|
if (! is_array($seams)) {
|
||||||
|
throw new InvalidArgumentException('Provider boundary seam catalog must be an array.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalog = [];
|
||||||
|
|
||||||
|
foreach ($seams as $key => $attributes) {
|
||||||
|
if (! is_string($key) || ! is_array($attributes)) {
|
||||||
|
throw new InvalidArgumentException('Provider boundary seam catalog entries must be keyed arrays.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$catalog[$key] = ProviderBoundarySeam::fromConfig($key, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($catalog);
|
||||||
|
|
||||||
|
return $catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key): ProviderBoundarySeam
|
||||||
|
{
|
||||||
|
$normalizedKey = trim($key);
|
||||||
|
$seam = $this->all()[$normalizedKey] ?? null;
|
||||||
|
|
||||||
|
if (! $seam instanceof ProviderBoundarySeam) {
|
||||||
|
throw new InvalidArgumentException("Unknown provider boundary seam: {$normalizedKey}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $seam;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $key): bool
|
||||||
|
{
|
||||||
|
return array_key_exists(trim($key), $this->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* status: string,
|
||||||
|
* seam_key: string,
|
||||||
|
* file_path: string,
|
||||||
|
* violation_code: string,
|
||||||
|
* message: string,
|
||||||
|
* suggested_follow_up: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function evaluateChange(
|
||||||
|
string $seamKey,
|
||||||
|
string $filePath,
|
||||||
|
ProviderBoundaryOwner|string $proposedOwner,
|
||||||
|
array $providerSpecificTerms = [],
|
||||||
|
bool $introducesNewBinding = false,
|
||||||
|
): array {
|
||||||
|
$seam = $this->get($seamKey);
|
||||||
|
$owner = is_string($proposedOwner)
|
||||||
|
? ProviderBoundaryOwner::tryFrom($proposedOwner)
|
||||||
|
: $proposedOwner;
|
||||||
|
|
||||||
|
if (! $owner instanceof ProviderBoundaryOwner) {
|
||||||
|
throw new InvalidArgumentException('Proposed provider boundary owner is invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$providerSpecificTerms = $this->normalizeTerms($providerSpecificTerms);
|
||||||
|
|
||||||
|
if ($introducesNewBinding && $seam->isPlatformCore() && $owner === ProviderBoundaryOwner::PlatformCore) {
|
||||||
|
return $this->result(
|
||||||
|
status: self::STATUS_BLOCKED,
|
||||||
|
seam: $seam,
|
||||||
|
filePath: $filePath,
|
||||||
|
violationCode: self::VIOLATION_PROVIDER_BINDING_AS_PRIMARY_TRUTH,
|
||||||
|
message: 'Provider binding metadata must stay explicit and secondary to the platform-core operation definition.',
|
||||||
|
suggestedFollowUp: ProviderBoundarySeam::FOLLOW_UP_DOCUMENT_IN_FEATURE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($seam->isProviderOwned()) {
|
||||||
|
return $this->result(
|
||||||
|
status: self::STATUS_ALLOWED,
|
||||||
|
seam: $seam,
|
||||||
|
filePath: $filePath,
|
||||||
|
violationCode: self::VIOLATION_NONE,
|
||||||
|
message: 'Provider-specific semantics are allowed inside this provider-owned seam.',
|
||||||
|
suggestedFollowUp: ProviderBoundarySeam::FOLLOW_UP_NONE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($providerSpecificTerms === []) {
|
||||||
|
return $this->result(
|
||||||
|
status: self::STATUS_ALLOWED,
|
||||||
|
seam: $seam,
|
||||||
|
filePath: $filePath,
|
||||||
|
violationCode: self::VIOLATION_NONE,
|
||||||
|
message: 'The platform-core seam does not introduce provider-specific terms.',
|
||||||
|
suggestedFollowUp: ProviderBoundarySeam::FOLLOW_UP_NONE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$undocumentedTerms = array_values(array_filter(
|
||||||
|
$providerSpecificTerms,
|
||||||
|
static fn (string $term): bool => ! $seam->documentsProviderSemantic($term),
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($undocumentedTerms !== []) {
|
||||||
|
return $this->result(
|
||||||
|
status: self::STATUS_BLOCKED,
|
||||||
|
seam: $seam,
|
||||||
|
filePath: $filePath,
|
||||||
|
violationCode: self::VIOLATION_PLATFORM_CORE_PROVIDER_LEAK,
|
||||||
|
message: 'Platform-core seam contains undocumented provider-specific terms: '.implode(', ', $undocumentedTerms).'.',
|
||||||
|
suggestedFollowUp: ProviderBoundarySeam::FOLLOW_UP_SPEC,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->result(
|
||||||
|
status: self::STATUS_REVIEW_REQUIRED,
|
||||||
|
seam: $seam,
|
||||||
|
filePath: $filePath,
|
||||||
|
violationCode: self::VIOLATION_NONE,
|
||||||
|
message: 'Platform-core seam relies on documented current-release provider exception metadata.',
|
||||||
|
suggestedFollowUp: $seam->followUpAction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $terms
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function normalizeTerms(array $terms): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
array_map(static fn (mixed $term): string => trim((string) $term), $terms),
|
||||||
|
static fn (string $term): bool => $term !== '',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* status: string,
|
||||||
|
* seam_key: string,
|
||||||
|
* file_path: string,
|
||||||
|
* violation_code: string,
|
||||||
|
* message: string,
|
||||||
|
* suggested_follow_up: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function result(
|
||||||
|
string $status,
|
||||||
|
ProviderBoundarySeam $seam,
|
||||||
|
string $filePath,
|
||||||
|
string $violationCode,
|
||||||
|
string $message,
|
||||||
|
string $suggestedFollowUp,
|
||||||
|
): array {
|
||||||
|
return [
|
||||||
|
'status' => $status,
|
||||||
|
'seam_key' => $seam->key,
|
||||||
|
'file_path' => $filePath,
|
||||||
|
'violation_code' => $violationCode,
|
||||||
|
'message' => $message,
|
||||||
|
'suggested_follow_up' => $suggestedFollowUp,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Providers\Boundary;
|
||||||
|
|
||||||
|
enum ProviderBoundaryOwner: string
|
||||||
|
{
|
||||||
|
case ProviderOwned = 'provider_owned';
|
||||||
|
case PlatformCore = 'platform_core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function values(): array
|
||||||
|
{
|
||||||
|
return array_map(static fn (self $case): string => $case->value, self::cases());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Providers\Boundary;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class ProviderBoundarySeam
|
||||||
|
{
|
||||||
|
public const string FOLLOW_UP_NONE = 'none';
|
||||||
|
|
||||||
|
public const string FOLLOW_UP_DOCUMENT_IN_FEATURE = 'document-in-feature';
|
||||||
|
|
||||||
|
public const string FOLLOW_UP_SPEC = 'follow-up-spec';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $implementationPaths
|
||||||
|
* @param list<string> $neutralTerms
|
||||||
|
* @param list<string> $retainedProviderSemantics
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $key,
|
||||||
|
public readonly ProviderBoundaryOwner $owner,
|
||||||
|
public readonly string $description,
|
||||||
|
public readonly array $implementationPaths,
|
||||||
|
public readonly array $neutralTerms,
|
||||||
|
public readonly array $retainedProviderSemantics,
|
||||||
|
public readonly string $followUpAction,
|
||||||
|
) {
|
||||||
|
$this->validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{
|
||||||
|
* owner?: string,
|
||||||
|
* description?: string,
|
||||||
|
* implementation_paths?: list<string>,
|
||||||
|
* neutral_terms?: list<string>,
|
||||||
|
* retained_provider_semantics?: list<string>,
|
||||||
|
* follow_up_action?: string
|
||||||
|
* } $attributes
|
||||||
|
*/
|
||||||
|
public static function fromConfig(string $key, array $attributes): self
|
||||||
|
{
|
||||||
|
$owner = ProviderBoundaryOwner::tryFrom((string) ($attributes['owner'] ?? ''));
|
||||||
|
|
||||||
|
if (! $owner instanceof ProviderBoundaryOwner) {
|
||||||
|
throw new InvalidArgumentException("Provider boundary seam [{$key}] has an invalid owner.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
key: $key,
|
||||||
|
owner: $owner,
|
||||||
|
description: (string) ($attributes['description'] ?? ''),
|
||||||
|
implementationPaths: self::stringList($attributes['implementation_paths'] ?? []),
|
||||||
|
neutralTerms: self::stringList($attributes['neutral_terms'] ?? []),
|
||||||
|
retainedProviderSemantics: self::stringList($attributes['retained_provider_semantics'] ?? []),
|
||||||
|
followUpAction: (string) ($attributes['follow_up_action'] ?? self::FOLLOW_UP_NONE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isProviderOwned(): bool
|
||||||
|
{
|
||||||
|
return $this->owner === ProviderBoundaryOwner::ProviderOwned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPlatformCore(): bool
|
||||||
|
{
|
||||||
|
return $this->owner === ProviderBoundaryOwner::PlatformCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function retainsProviderSemantics(): bool
|
||||||
|
{
|
||||||
|
return $this->retainedProviderSemantics !== [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function documentsProviderSemantic(string $term): bool
|
||||||
|
{
|
||||||
|
return in_array($term, $this->retainedProviderSemantics, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function coversPath(string $path): bool
|
||||||
|
{
|
||||||
|
$normalizedPath = $this->normalizePath($path);
|
||||||
|
|
||||||
|
foreach ($this->implementationPaths as $implementationPath) {
|
||||||
|
if ($normalizedPath === $this->normalizePath($implementationPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $values
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private static function stringList(array $values): array
|
||||||
|
{
|
||||||
|
return array_values(array_filter(
|
||||||
|
array_map(static fn (mixed $value): string => trim((string) $value), $values),
|
||||||
|
static fn (string $value): bool => $value !== '',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validate(): void
|
||||||
|
{
|
||||||
|
if (trim($this->key) === '') {
|
||||||
|
throw new InvalidArgumentException('Provider boundary seam key cannot be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim($this->description) === '') {
|
||||||
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] must include a description.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->implementationPaths === []) {
|
||||||
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] must include implementation paths.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isPlatformCore() && $this->neutralTerms === []) {
|
||||||
|
throw new InvalidArgumentException("Platform-core provider boundary seam [{$this->key}] must include neutral terms.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->retainsProviderSemantics() && $this->followUpAction === self::FOLLOW_UP_NONE) {
|
||||||
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] retains provider semantics without a follow-up action.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($this->followUpAction, $this->validFollowUpActions(), true)) {
|
||||||
|
throw new InvalidArgumentException("Provider boundary seam [{$this->key}] has an invalid follow-up action.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function validFollowUpActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::FOLLOW_UP_NONE,
|
||||||
|
self::FOLLOW_UP_DOCUMENT_IN_FEATURE,
|
||||||
|
self::FOLLOW_UP_SPEC,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePath(string $path): string
|
||||||
|
{
|
||||||
|
return trim(str_replace('\\', '/', $path), '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,6 +34,8 @@ final class ProviderReasonCodes
|
|||||||
|
|
||||||
public const string ProviderConnectionReviewRequired = 'provider_connection_review_required';
|
public const string ProviderConnectionReviewRequired = 'provider_connection_review_required';
|
||||||
|
|
||||||
|
public const string ProviderBindingUnsupported = 'provider_binding_unsupported';
|
||||||
|
|
||||||
public const string ProviderAuthFailed = 'provider_auth_failed';
|
public const string ProviderAuthFailed = 'provider_auth_failed';
|
||||||
|
|
||||||
public const string ProviderPermissionMissing = 'provider_permission_missing';
|
public const string ProviderPermissionMissing = 'provider_permission_missing';
|
||||||
@ -77,6 +79,7 @@ public static function all(): array
|
|||||||
self::ProviderConsentFailed,
|
self::ProviderConsentFailed,
|
||||||
self::ProviderConsentRevoked,
|
self::ProviderConsentRevoked,
|
||||||
self::ProviderConnectionReviewRequired,
|
self::ProviderConnectionReviewRequired,
|
||||||
|
self::ProviderBindingUnsupported,
|
||||||
self::ProviderAuthFailed,
|
self::ProviderAuthFailed,
|
||||||
self::ProviderPermissionMissing,
|
self::ProviderPermissionMissing,
|
||||||
self::ProviderPermissionDenied,
|
self::ProviderPermissionDenied,
|
||||||
@ -139,6 +142,7 @@ public static function platformReasonFamily(string $reasonCode): PlatformReasonF
|
|||||||
self::ProviderAuthFailed => PlatformReasonFamily::Availability,
|
self::ProviderAuthFailed => PlatformReasonFamily::Availability,
|
||||||
self::ProviderConnectionTypeInvalid,
|
self::ProviderConnectionTypeInvalid,
|
||||||
self::TenantTargetMismatch,
|
self::TenantTargetMismatch,
|
||||||
|
self::ProviderBindingUnsupported,
|
||||||
self::ProviderConnectionReviewRequired => PlatformReasonFamily::Compatibility,
|
self::ProviderConnectionReviewRequired => PlatformReasonFamily::Compatibility,
|
||||||
default => PlatformReasonFamily::Prerequisite,
|
default => PlatformReasonFamily::Prerequisite,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -141,6 +141,13 @@ public function translate(string $reasonCode, string $surface = 'detail', array
|
|||||||
actionability: 'prerequisite_missing',
|
actionability: 'prerequisite_missing',
|
||||||
nextSteps: $nextSteps,
|
nextSteps: $nextSteps,
|
||||||
),
|
),
|
||||||
|
ProviderReasonCodes::ProviderBindingUnsupported => $this->envelope(
|
||||||
|
reasonCode: $normalizedCode,
|
||||||
|
operatorLabel: 'Provider binding unsupported',
|
||||||
|
shortExplanation: 'This operation does not have an explicit provider binding for the selected provider.',
|
||||||
|
actionability: 'permanent_configuration',
|
||||||
|
nextSteps: $nextSteps,
|
||||||
|
),
|
||||||
ProviderReasonCodes::ProviderAuthFailed => $this->envelope(
|
ProviderReasonCodes::ProviderAuthFailed => $this->envelope(
|
||||||
reasonCode: $normalizedCode,
|
reasonCode: $normalizedCode,
|
||||||
operatorLabel: 'Provider authentication failed',
|
operatorLabel: 'Provider authentication failed',
|
||||||
@ -284,7 +291,8 @@ private function nextStepsFor(
|
|||||||
ProviderReasonCodes::TenantTargetMismatch,
|
ProviderReasonCodes::TenantTargetMismatch,
|
||||||
ProviderReasonCodes::PlatformIdentityMissing,
|
ProviderReasonCodes::PlatformIdentityMissing,
|
||||||
ProviderReasonCodes::PlatformIdentityIncomplete,
|
ProviderReasonCodes::PlatformIdentityIncomplete,
|
||||||
ProviderReasonCodes::ProviderConnectionReviewRequired => [
|
ProviderReasonCodes::ProviderConnectionReviewRequired,
|
||||||
|
ProviderReasonCodes::ProviderBindingUnsupported => [
|
||||||
NextStepOption::link(
|
NextStepOption::link(
|
||||||
label: $connection instanceof ProviderConnection ? 'Review migration classification' : 'Manage Provider Connections',
|
label: $connection instanceof ProviderConnection ? 'Review migration classification' : 'Manage Provider Connections',
|
||||||
destination: $connection instanceof ProviderConnection
|
destination: $connection instanceof ProviderConnection
|
||||||
|
|||||||
115
apps/platform/config/provider_boundaries.php
Normal file
115
apps/platform/config/provider_boundaries.php
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundaryOwner;
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundarySeam;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'seams' => [
|
||||||
|
'provider.gateway_runtime' => [
|
||||||
|
'owner' => ProviderBoundaryOwner::ProviderOwned->value,
|
||||||
|
'description' => 'Provider-owned runtime boundary that translates provider connection identity into Microsoft Graph request options and executes Graph calls.',
|
||||||
|
'implementation_paths' => [
|
||||||
|
'app/Services/Providers/ProviderGateway.php',
|
||||||
|
'app/Services/Providers/MicrosoftGraphOptionsResolver.php',
|
||||||
|
],
|
||||||
|
'neutral_terms' => [
|
||||||
|
'provider',
|
||||||
|
'provider connection',
|
||||||
|
'target scope',
|
||||||
|
'runtime request context',
|
||||||
|
],
|
||||||
|
'retained_provider_semantics' => [
|
||||||
|
'Microsoft Graph option keys',
|
||||||
|
'client_request_id',
|
||||||
|
'tenant',
|
||||||
|
'client_id',
|
||||||
|
'client_secret',
|
||||||
|
],
|
||||||
|
'follow_up_action' => ProviderBoundarySeam::FOLLOW_UP_DOCUMENT_IN_FEATURE,
|
||||||
|
],
|
||||||
|
'provider.identity_resolution' => [
|
||||||
|
'owner' => ProviderBoundaryOwner::PlatformCore->value,
|
||||||
|
'description' => 'Platform-core identity resolution contract that resolves provider connection identity without owning provider transport option shaping.',
|
||||||
|
'implementation_paths' => [
|
||||||
|
'app/Services/Providers/ProviderIdentityResolution.php',
|
||||||
|
'app/Services/Providers/ProviderIdentityResolver.php',
|
||||||
|
'app/Services/Providers/PlatformProviderIdentityResolver.php',
|
||||||
|
],
|
||||||
|
'neutral_terms' => [
|
||||||
|
'provider connection',
|
||||||
|
'target scope',
|
||||||
|
'credential source',
|
||||||
|
'effective client identity',
|
||||||
|
],
|
||||||
|
'retained_provider_semantics' => [
|
||||||
|
'entra_tenant_id',
|
||||||
|
'platform_config',
|
||||||
|
'graph.tenant_id',
|
||||||
|
'admin.consent.callback',
|
||||||
|
],
|
||||||
|
'follow_up_action' => ProviderBoundarySeam::FOLLOW_UP_SPEC,
|
||||||
|
],
|
||||||
|
'provider.connection_resolution' => [
|
||||||
|
'owner' => ProviderBoundaryOwner::PlatformCore->value,
|
||||||
|
'description' => 'Platform-core provider connection selection and validation path that keeps current Microsoft connection details as bounded exception metadata.',
|
||||||
|
'implementation_paths' => [
|
||||||
|
'app/Services/Providers/ProviderConnectionResolver.php',
|
||||||
|
'app/Services/Providers/ProviderConnectionResolution.php',
|
||||||
|
],
|
||||||
|
'neutral_terms' => [
|
||||||
|
'provider',
|
||||||
|
'provider connection',
|
||||||
|
'tenant scope',
|
||||||
|
'default binding',
|
||||||
|
'unsupported combination',
|
||||||
|
],
|
||||||
|
'retained_provider_semantics' => [
|
||||||
|
'microsoft',
|
||||||
|
'entra_tenant_id',
|
||||||
|
'consent_status',
|
||||||
|
],
|
||||||
|
'follow_up_action' => ProviderBoundarySeam::FOLLOW_UP_SPEC,
|
||||||
|
],
|
||||||
|
'provider.operation_registry' => [
|
||||||
|
'owner' => ProviderBoundaryOwner::PlatformCore->value,
|
||||||
|
'description' => 'Platform-core operation definition catalog with provider binding metadata kept explicit and secondary.',
|
||||||
|
'implementation_paths' => [
|
||||||
|
'app/Services/Providers/ProviderOperationRegistry.php',
|
||||||
|
],
|
||||||
|
'neutral_terms' => [
|
||||||
|
'operation type',
|
||||||
|
'operation module',
|
||||||
|
'required capability',
|
||||||
|
'provider binding',
|
||||||
|
'unsupported binding',
|
||||||
|
],
|
||||||
|
'retained_provider_semantics' => [
|
||||||
|
'microsoft',
|
||||||
|
'active provider binding',
|
||||||
|
'binding_status',
|
||||||
|
'handler_notes',
|
||||||
|
'exception_notes',
|
||||||
|
],
|
||||||
|
'follow_up_action' => ProviderBoundarySeam::FOLLOW_UP_DOCUMENT_IN_FEATURE,
|
||||||
|
],
|
||||||
|
'provider.operation_start_gate' => [
|
||||||
|
'owner' => ProviderBoundaryOwner::PlatformCore->value,
|
||||||
|
'description' => 'Platform-core operation start orchestration that consumes explicit provider bindings and records current Microsoft target-scope exceptions.',
|
||||||
|
'implementation_paths' => [
|
||||||
|
'app/Services/Providers/ProviderOperationStartGate.php',
|
||||||
|
],
|
||||||
|
'neutral_terms' => [
|
||||||
|
'operation',
|
||||||
|
'provider binding',
|
||||||
|
'target scope',
|
||||||
|
'execution authority',
|
||||||
|
'required capability',
|
||||||
|
],
|
||||||
|
'retained_provider_semantics' => [
|
||||||
|
'microsoft',
|
||||||
|
'target_scope.entra_tenant_id',
|
||||||
|
],
|
||||||
|
'follow_up_action' => ProviderBoundarySeam::FOLLOW_UP_SPEC,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\Providers\ProviderOperationRegistry;
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundaryCatalog;
|
||||||
|
|
||||||
|
it('blocks graph request option helpers from platform-core identity resolution seams', function (): void {
|
||||||
|
$root = base_path();
|
||||||
|
$catalog = app(ProviderBoundaryCatalog::class);
|
||||||
|
$seam = $catalog->get('provider.identity_resolution');
|
||||||
|
|
||||||
|
$hits = [];
|
||||||
|
|
||||||
|
foreach ($seam->implementationPaths as $relativePath) {
|
||||||
|
if ($relativePath !== 'app/Services/Providers/ProviderIdentityResolution.php') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $root.'/'.$relativePath;
|
||||||
|
$contents = file_get_contents($path);
|
||||||
|
|
||||||
|
if (! is_string($contents)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['graphOptions', 'client_request_id'] as $forbiddenTerm) {
|
||||||
|
if (str_contains($contents, $forbiddenTerm)) {
|
||||||
|
$hits[] = $relativePath.' contains '.$forbiddenTerm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($hits)->toBeEmpty('Platform-core identity resolution must not shape Microsoft Graph request options.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps provider bindings out of platform-core operation definitions', function (): void {
|
||||||
|
$registry = app(ProviderOperationRegistry::class);
|
||||||
|
|
||||||
|
foreach ($registry->definitions() as $operationType => $definition) {
|
||||||
|
expect($definition)
|
||||||
|
->not->toHaveKey('provider')
|
||||||
|
->not->toHaveKey('binding_status')
|
||||||
|
->not->toHaveKey('handler_notes')
|
||||||
|
->not->toHaveKey('exception_notes');
|
||||||
|
|
||||||
|
expect($registry->bindingFor($operationType, 'microsoft'))->toBeArray();
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\ProviderConnection;
|
||||||
|
use App\Models\ProviderCredential;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Services\Graph\GraphClientInterface;
|
||||||
|
use App\Services\Graph\GraphResponse;
|
||||||
|
use App\Services\Providers\MicrosoftGraphOptionsResolver;
|
||||||
|
use App\Services\Providers\ProviderGateway;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('preserves current Microsoft-backed gateway runtime behavior through the provider-owned seam', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$connection = ProviderConnection::factory()->dedicated()->consentGranted()->create([
|
||||||
|
'tenant_id' => $tenant->getKey(),
|
||||||
|
'workspace_id' => $tenant->workspace_id,
|
||||||
|
'provider' => 'microsoft',
|
||||||
|
'entra_tenant_id' => 'entra-tenant-id',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProviderCredential::factory()->create([
|
||||||
|
'provider_connection_id' => $connection->getKey(),
|
||||||
|
'payload' => [
|
||||||
|
'client_id' => 'client-id',
|
||||||
|
'client_secret' => 'client-secret',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$graph = new class implements GraphClientInterface
|
||||||
|
{
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $options = [];
|
||||||
|
|
||||||
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
$this->options = $options;
|
||||||
|
|
||||||
|
return new GraphResponse(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrganization(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$gateway = app()->make(ProviderGateway::class, [
|
||||||
|
'graph' => $graph,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gateway->listPolicies($connection, 'deviceConfiguration', ['query' => ['$select' => 'id']]);
|
||||||
|
|
||||||
|
expect($graph->options)->toMatchArray([
|
||||||
|
'tenant' => 'entra-tenant-id',
|
||||||
|
'client_id' => 'client-id',
|
||||||
|
'client_secret' => 'client-secret',
|
||||||
|
'query' => ['$select' => 'id'],
|
||||||
|
]);
|
||||||
|
expect($graph->options['client_request_id'])->toBeString()->not->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves Microsoft graph option resolution for default provider connections', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$connection = ProviderConnection::factory()->dedicated()->consentGranted()->create([
|
||||||
|
'tenant_id' => $tenant->getKey(),
|
||||||
|
'workspace_id' => $tenant->workspace_id,
|
||||||
|
'provider' => 'microsoft',
|
||||||
|
'entra_tenant_id' => 'default-entra-tenant-id',
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProviderCredential::factory()->create([
|
||||||
|
'provider_connection_id' => $connection->getKey(),
|
||||||
|
'payload' => [
|
||||||
|
'client_id' => 'default-client-id',
|
||||||
|
'client_secret' => 'default-client-secret',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$options = app(MicrosoftGraphOptionsResolver::class)->resolveForTenant($tenant);
|
||||||
|
|
||||||
|
expect($options['tenant'])->toBe('default-entra-tenant-id')
|
||||||
|
->and($options['client_id'])->toBe('default-client-id')
|
||||||
|
->and($options['client_secret'])->toBe('default-client-secret')
|
||||||
|
->and($options['client_request_id'])->toBeString()->not->toBeEmpty();
|
||||||
|
});
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\ProviderConnection;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Services\Providers\MicrosoftGraphOptionsResolver;
|
||||||
|
use App\Services\Providers\ProviderConfigurationRequiredException;
|
||||||
|
use App\Services\Providers\ProviderGateway;
|
||||||
|
use App\Support\Providers\ProviderReasonCodes;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('fails explicitly when provider-owned graph option assembly cannot resolve identity', function (): void {
|
||||||
|
$connection = ProviderConnection::factory()->dedicated()->consentGranted()->create([
|
||||||
|
'entra_tenant_id' => 'entra-tenant-id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$call = fn (): array => app(ProviderGateway::class)->graphOptions($connection);
|
||||||
|
|
||||||
|
expect($call)->toThrow(RuntimeException::class, 'Provider credentials are missing.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not inherit Microsoft runtime behavior when a default provider connection is unsupported', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
ProviderConnection::factory()->dedicated()->create([
|
||||||
|
'tenant_id' => $tenant->getKey(),
|
||||||
|
'workspace_id' => $tenant->workspace_id,
|
||||||
|
'provider' => 'microsoft',
|
||||||
|
'entra_tenant_id' => 'entra-tenant-id',
|
||||||
|
'is_default' => true,
|
||||||
|
'consent_status' => 'required',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$call = fn (): array => app(MicrosoftGraphOptionsResolver::class)->resolveForTenant($tenant);
|
||||||
|
|
||||||
|
expect($call)->toThrow(ProviderConfigurationRequiredException::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$call();
|
||||||
|
} catch (ProviderConfigurationRequiredException $exception) {
|
||||||
|
expect($exception->reasonCode)->toBe(ProviderReasonCodes::ProviderConsentMissing)
|
||||||
|
->and($exception->provider)->toBe('microsoft');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundaryCatalog;
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundaryOwner;
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundarySeam;
|
||||||
|
|
||||||
|
it('classifies the authoritative first-slice provider seams', function (): void {
|
||||||
|
$catalog = app(ProviderBoundaryCatalog::class);
|
||||||
|
|
||||||
|
expect(array_keys($catalog->all()))->toBe([
|
||||||
|
'provider.connection_resolution',
|
||||||
|
'provider.gateway_runtime',
|
||||||
|
'provider.identity_resolution',
|
||||||
|
'provider.operation_registry',
|
||||||
|
'provider.operation_start_gate',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($catalog->get('provider.gateway_runtime')->owner)->toBe(ProviderBoundaryOwner::ProviderOwned);
|
||||||
|
expect($catalog->get('provider.identity_resolution')->owner)->toBe(ProviderBoundaryOwner::PlatformCore);
|
||||||
|
expect($catalog->get('provider.connection_resolution')->owner)->toBe(ProviderBoundaryOwner::PlatformCore);
|
||||||
|
expect($catalog->get('provider.operation_registry')->owner)->toBe(ProviderBoundaryOwner::PlatformCore);
|
||||||
|
expect($catalog->get('provider.operation_start_gate')->owner)->toBe(ProviderBoundaryOwner::PlatformCore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records implementation paths and bounded exception metadata for platform-core seams', function (): void {
|
||||||
|
$catalog = app(ProviderBoundaryCatalog::class);
|
||||||
|
|
||||||
|
$identity = $catalog->get('provider.identity_resolution');
|
||||||
|
|
||||||
|
expect($identity->coversPath('app/Services/Providers/ProviderIdentityResolution.php'))->toBeTrue()
|
||||||
|
->and($identity->neutralTerms)->toContain('target scope')
|
||||||
|
->and($identity->retainedProviderSemantics)->toContain('entra_tenant_id')
|
||||||
|
->and($identity->retainedProviderSemantics)->not->toContain('Microsoft Graph option keys')
|
||||||
|
->and($identity->followUpAction)->toBe(ProviderBoundarySeam::FOLLOW_UP_SPEC);
|
||||||
|
|
||||||
|
$registry = $catalog->get('provider.operation_registry');
|
||||||
|
|
||||||
|
expect($registry->neutralTerms)->toContain('provider binding')
|
||||||
|
->and($registry->retainedProviderSemantics)->toContain('active provider binding')
|
||||||
|
->and($registry->followUpAction)->toBe(ProviderBoundarySeam::FOLLOW_UP_DOCUMENT_IN_FEATURE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('aligns the catalog shape with the logical boundary contract', function (): void {
|
||||||
|
$seam = app(ProviderBoundaryCatalog::class)->get('provider.operation_start_gate');
|
||||||
|
|
||||||
|
expect($seam->key)->toBe('provider.operation_start_gate')
|
||||||
|
->and($seam->owner->value)->toBeIn(ProviderBoundaryOwner::values())
|
||||||
|
->and($seam->description)->not->toBeEmpty()
|
||||||
|
->and($seam->implementationPaths)->not->toBeEmpty()
|
||||||
|
->and($seam->neutralTerms)->not->toBeEmpty()
|
||||||
|
->and($seam->retainedProviderSemantics)->toContain('target_scope.entra_tenant_id')
|
||||||
|
->and($seam->followUpAction)->toBeIn([
|
||||||
|
ProviderBoundarySeam::FOLLOW_UP_NONE,
|
||||||
|
ProviderBoundarySeam::FOLLOW_UP_DOCUMENT_IN_FEATURE,
|
||||||
|
ProviderBoundarySeam::FOLLOW_UP_SPEC,
|
||||||
|
]);
|
||||||
|
});
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\Providers\ProviderOperationRegistry;
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundaryCatalog;
|
||||||
|
use App\Support\Providers\Boundary\ProviderBoundaryOwner;
|
||||||
|
|
||||||
|
it('blocks undocumented provider terms in platform-core seams', function (): void {
|
||||||
|
$result = app(ProviderBoundaryCatalog::class)->evaluateChange(
|
||||||
|
seamKey: 'provider.identity_resolution',
|
||||||
|
filePath: 'app/Services/Providers/ProviderIdentityResolution.php',
|
||||||
|
proposedOwner: ProviderBoundaryOwner::PlatformCore,
|
||||||
|
providerSpecificTerms: ['client_request_id'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result['status'])->toBe(ProviderBoundaryCatalog::STATUS_BLOCKED)
|
||||||
|
->and($result['violation_code'])->toBe(ProviderBoundaryCatalog::VIOLATION_PLATFORM_CORE_PROVIDER_LEAK)
|
||||||
|
->and($result['suggested_follow_up'])->toBe('follow-up-spec');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requires review for documented current-release exceptions on platform-core seams', function (): void {
|
||||||
|
$result = app(ProviderBoundaryCatalog::class)->evaluateChange(
|
||||||
|
seamKey: 'provider.identity_resolution',
|
||||||
|
filePath: 'app/Services/Providers/ProviderIdentityResolver.php',
|
||||||
|
proposedOwner: 'platform_core',
|
||||||
|
providerSpecificTerms: ['entra_tenant_id'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result['status'])->toBe(ProviderBoundaryCatalog::STATUS_REVIEW_REQUIRED)
|
||||||
|
->and($result['violation_code'])->toBe(ProviderBoundaryCatalog::VIOLATION_NONE)
|
||||||
|
->and($result['suggested_follow_up'])->toBe('follow-up-spec');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows provider-specific terms inside provider-owned seams', function (): void {
|
||||||
|
$result = app(ProviderBoundaryCatalog::class)->evaluateChange(
|
||||||
|
seamKey: 'provider.gateway_runtime',
|
||||||
|
filePath: 'app/Services/Providers/ProviderGateway.php',
|
||||||
|
proposedOwner: ProviderBoundaryOwner::ProviderOwned,
|
||||||
|
providerSpecificTerms: ['client_request_id', 'client_secret'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result['status'])->toBe(ProviderBoundaryCatalog::STATUS_ALLOWED)
|
||||||
|
->and($result['violation_code'])->toBe(ProviderBoundaryCatalog::VIOLATION_NONE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps operation definitions separate from provider bindings', function (): void {
|
||||||
|
$registry = app(ProviderOperationRegistry::class);
|
||||||
|
|
||||||
|
$definition = $registry->get('provider.connection.check');
|
||||||
|
$binding = $registry->bindingFor('provider.connection.check', 'microsoft');
|
||||||
|
|
||||||
|
expect($definition)->toMatchArray([
|
||||||
|
'operation_type' => 'provider.connection.check',
|
||||||
|
'module' => 'health_check',
|
||||||
|
'label' => 'Provider connection check',
|
||||||
|
'required_capability' => \App\Support\Auth\Capabilities::PROVIDER_RUN,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($binding)->toMatchArray([
|
||||||
|
'provider' => 'microsoft',
|
||||||
|
'binding_status' => ProviderOperationRegistry::BINDING_ACTIVE,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks provider binding metadata when it is proposed as platform-core truth', function (): void {
|
||||||
|
$result = app(ProviderBoundaryCatalog::class)->evaluateChange(
|
||||||
|
seamKey: 'provider.operation_registry',
|
||||||
|
filePath: 'app/Services/Providers/ProviderOperationRegistry.php',
|
||||||
|
proposedOwner: ProviderBoundaryOwner::PlatformCore,
|
||||||
|
providerSpecificTerms: ['microsoft'],
|
||||||
|
introducesNewBinding: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result['status'])->toBe(ProviderBoundaryCatalog::STATUS_BLOCKED)
|
||||||
|
->and($result['violation_code'])->toBe(ProviderBoundaryCatalog::VIOLATION_PROVIDER_BINDING_AS_PRIMARY_TRUTH);
|
||||||
|
});
|
||||||
@ -183,3 +183,31 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
->and($graph->lastOptions['client_id'] ?? null)->not->toBe('dedicated-fallback-client-id')
|
->and($graph->lastOptions['client_id'] ?? null)->not->toBe('dedicated-fallback-client-id')
|
||||||
->and($graph->lastOptions['client_request_id'] ?? null)->toBeString()->not->toBeEmpty();
|
->and($graph->lastOptions['client_request_id'] ?? null)->toBeString()->not->toBeEmpty();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('owns graph request option assembly for resolved provider identities', function (): void {
|
||||||
|
$connection = ProviderConnection::factory()->dedicated()->create([
|
||||||
|
'entra_tenant_id' => 'entra-tenant-id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProviderCredential::factory()->create([
|
||||||
|
'provider_connection_id' => $connection->getKey(),
|
||||||
|
'payload' => [
|
||||||
|
'client_id' => 'client-id',
|
||||||
|
'client_secret' => 'client-secret',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$gateway = app(ProviderGateway::class);
|
||||||
|
|
||||||
|
$options = $gateway->graphOptions($connection, [
|
||||||
|
'query' => ['$select' => 'id'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect($options)->toMatchArray([
|
||||||
|
'tenant' => 'entra-tenant-id',
|
||||||
|
'client_id' => 'client-id',
|
||||||
|
'client_secret' => 'client-secret',
|
||||||
|
'query' => ['$select' => 'id'],
|
||||||
|
]);
|
||||||
|
expect($options['client_request_id'])->toBeString()->not->toBeEmpty();
|
||||||
|
});
|
||||||
|
|||||||
@ -72,6 +72,27 @@
|
|||||||
->and($resolution->credentialSource)->toBe('dedicated_manual');
|
->and($resolution->credentialSource)->toBe('dedicated_manual');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps graph request option shaping out of identity resolution results', function (): void {
|
||||||
|
$connection = ProviderConnection::factory()->dedicated()->create([
|
||||||
|
'entra_tenant_id' => 'dedicated-target-tenant-id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ProviderCredential::factory()->create([
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
'payload' => [
|
||||||
|
'client_id' => 'dedicated-client-id',
|
||||||
|
'client_secret' => 'dedicated-client-secret',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolution = app(ProviderIdentityResolver::class)->resolve($connection);
|
||||||
|
|
||||||
|
expect($resolution->resolved)->toBeTrue()
|
||||||
|
->and($resolution->tenantContext)->toBe('dedicated-target-tenant-id')
|
||||||
|
->and($resolution->effectiveClientId)->toBe('dedicated-client-id')
|
||||||
|
->and(method_exists($resolution, 'graphOptions'))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
it('blocks dedicated connections when dedicated credentials are missing', function (): void {
|
it('blocks dedicated connections when dedicated credentials are missing', function (): void {
|
||||||
$connection = ProviderConnection::factory()->dedicated()->create([
|
$connection = ProviderConnection::factory()->dedicated()->create([
|
||||||
'entra_tenant_id' => 'dedicated-target-tenant-id',
|
'entra_tenant_id' => 'dedicated-target-tenant-id',
|
||||||
|
|||||||
@ -57,6 +57,8 @@
|
|||||||
'entra_tenant_id' => 'entra-tenant-id',
|
'entra_tenant_id' => 'entra-tenant-id',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
expect($run->context['provider_binding']['provider'] ?? null)->toBe('microsoft')
|
||||||
|
->and($run->context['provider_binding']['binding_status'] ?? null)->toBe('active');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dedupes when the same operation is already active for the scope', function (): void {
|
it('dedupes when the same operation is already active for the scope', function (): void {
|
||||||
@ -278,3 +280,39 @@
|
|||||||
expect($result->status)->toBe('scope_busy');
|
expect($result->status)->toBe('scope_busy');
|
||||||
expect($result->run->getKey())->toBe($blocking->getKey());
|
expect($result->run->getKey())->toBe($blocking->getKey());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('blocks provider starts when no explicit provider binding supports the connection provider', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
$connection = ProviderConnection::factory()->dedicated()->consentGranted()->create([
|
||||||
|
'tenant_id' => $tenant->getKey(),
|
||||||
|
'provider' => 'contoso',
|
||||||
|
'entra_tenant_id' => 'contoso-tenant-id',
|
||||||
|
'consent_status' => 'granted',
|
||||||
|
]);
|
||||||
|
ProviderCredential::factory()->create([
|
||||||
|
'provider_connection_id' => (int) $connection->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$dispatched = 0;
|
||||||
|
$gate = app(ProviderOperationStartGate::class);
|
||||||
|
|
||||||
|
$result = $gate->start(
|
||||||
|
tenant: $tenant,
|
||||||
|
connection: $connection,
|
||||||
|
operationType: 'provider.connection.check',
|
||||||
|
dispatcher: function () use (&$dispatched): void {
|
||||||
|
$dispatched++;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($dispatched)->toBe(0);
|
||||||
|
expect($result->status)->toBe('blocked');
|
||||||
|
expect($result->run->context)->toMatchArray([
|
||||||
|
'provider' => 'contoso',
|
||||||
|
'module' => 'health_check',
|
||||||
|
'reason_code' => ProviderReasonCodes::ProviderBindingUnsupported,
|
||||||
|
'reason_code_extension' => 'ext.provider_binding_missing',
|
||||||
|
]);
|
||||||
|
expect($result->run->context['provider_binding']['provider'] ?? null)->toBe('contoso')
|
||||||
|
->and($result->run->context['provider_binding']['binding_status'] ?? null)->toBe('unsupported');
|
||||||
|
});
|
||||||
|
|||||||
@ -25,7 +25,7 @@ ### Governance & Architecture Hardening
|
|||||||
Goal: Turn the new audit constitution into enforceable backend and workflow guardrails before further governance surface area lands, while preventing the Governance-of-Record platform core from drifting into provider-specific or operation-type dual semantics.
|
Goal: Turn the new audit constitution into enforceable backend and workflow guardrails before further governance surface area lands, while preventing the Governance-of-Record platform core from drifting into provider-specific or operation-type dual semantics.
|
||||||
|
|
||||||
**Active specs**: 144
|
**Active specs**: 144
|
||||||
**Specced follow-through (draft)**: 149 (queued execution reauthorization), 150 (tenant-owned query canon), 151 (findings workflow backstop), 152 (Livewire context locking), 214 (governance outcome compression), 216 (provider dispatch gate). Next foundation candidates: Canonical Operation Type Source of Truth, Provider Boundary Hardening, Provider Identity & Target Scope Neutrality, Platform Vocabulary Boundary Enforcement for Governed Subject Keys.
|
**Specced follow-through (draft)**: 149 (queued execution reauthorization), 150 (tenant-owned query canon), 151 (findings workflow backstop), 152 (Livewire context locking), 214 (governance outcome compression), 216 (provider dispatch gate), 237 (provider boundary hardening). Next foundation candidates: Canonical Operation Type Source of Truth, Provider Identity & Target Scope Neutrality, Platform Vocabulary Boundary Enforcement for Governed Subject Keys.
|
||||||
**Operator truth initiative** (sequenced): Operator Outcome Taxonomy (Spec 156) → Reason Code Translation (Spec 157) → Artifact Truth Semantics (Spec 158) → Governance Operator Outcome Compression (Spec 214, draft). Humanized Diagnostic Summaries for Governance Operations is now Spec 220 (draft) as the run-detail adoption slice, while Provider Dispatch Gate Unification is now Spec 216 (draft) as the adjacent hardening lane.
|
**Operator truth initiative** (sequenced): Operator Outcome Taxonomy (Spec 156) → Reason Code Translation (Spec 157) → Artifact Truth Semantics (Spec 158) → Governance Operator Outcome Compression (Spec 214, draft). Humanized Diagnostic Summaries for Governance Operations is now Spec 220 (draft) as the run-detail adoption slice, while Provider Dispatch Gate Unification is now Spec 216 (draft) as the adjacent hardening lane.
|
||||||
**Source**: architecture audit 2026-03-15, audit constitution, semantic clarity audit 2026-03-21, product spec-candidates
|
**Source**: architecture audit 2026-03-15, audit constitution, semantic clarity audit 2026-03-21, product spec-candidates
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ # Spec Candidates
|
|||||||
>
|
>
|
||||||
> **Flow**: Inbox → Qualified → Planned → Spec created → moved to `Promoted to Spec`
|
> **Flow**: Inbox → Qualified → Planned → Spec created → moved to `Promoted to Spec`
|
||||||
|
|
||||||
**Last reviewed**: 2026-04-24 (added a strategic Governance Platform Foundation priority note, clarified the near-term sequencing for control-catalog, provider-boundary, operation-type, and governed-subject hardening, and added `Customer Review Workspace v1` as the customer-facing review consumption candidate that sharpens the R2 read-only/customer review lane)
|
**Last reviewed**: 2026-04-24 (added Platform Hardening — OperationRun UX Consistency cluster with OperationRun Start UX Contract, Generic Active Run Surface, OperationRun Notification Lifecycle, and OperationRun Startsurface Migration; promoted Provider Boundary Hardening to Spec 237, clarified the remaining near-term sequencing after Canonical Control Catalog Foundation and Provider Boundary Hardening, and retained `Customer Review Workspace v1` as the customer-facing review consumption candidate that sharpens the R2 read-only/customer review lane)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -52,6 +52,7 @@ ## Promoted to Spec
|
|||||||
- Finding Outcome Taxonomy & Verification Semantics → Spec 231 (`finding-outcome-taxonomy`)
|
- Finding Outcome Taxonomy & Verification Semantics → Spec 231 (`finding-outcome-taxonomy`)
|
||||||
- Operation Run Link Contract Enforcement → Spec 232 (`operation-run-link-contract`)
|
- Operation Run Link Contract Enforcement → Spec 232 (`operation-run-link-contract`)
|
||||||
- Operation Run Active-State Visibility & Stale Escalation → Spec 233 (`stale-run-visibility`)
|
- Operation Run Active-State Visibility & Stale Escalation → Spec 233 (`stale-run-visibility`)
|
||||||
|
- Provider Boundary Hardening → Spec 237 (`provider-boundary-hardening`)
|
||||||
- Provider-Backed Action Preflight and Dispatch Gate Unification → Spec 216 (`provider-dispatch-gate`)
|
- Provider-Backed Action Preflight and Dispatch Gate Unification → Spec 216 (`provider-dispatch-gate`)
|
||||||
- Record Page Header Discipline & Contextual Navigation → Spec 192 (`record-header-discipline`)
|
- Record Page Header Discipline & Contextual Navigation → Spec 192 (`record-header-discipline`)
|
||||||
- Monitoring Surface Action Hierarchy & Workbench Semantics → Spec 193 (`monitoring-action-hierarchy`)
|
- Monitoring Surface Action Hierarchy & Workbench Semantics → Spec 193 (`monitoring-action-hierarchy`)
|
||||||
@ -69,33 +70,137 @@ ## Qualified
|
|||||||
>
|
>
|
||||||
> Recommended next sequence:
|
> Recommended next sequence:
|
||||||
>
|
>
|
||||||
> 1. **Canonical Control Catalog Foundation**
|
> 1. **Provider Identity & Target Scope Neutrality**
|
||||||
> 2. **Provider Boundary Hardening**
|
> 2. **Canonical Operation Type Source of Truth**
|
||||||
> 3. **Provider Identity & Target Scope Neutrality**
|
> 3. **Platform Vocabulary Boundary Enforcement for Governed Subject Keys**
|
||||||
> 4. **Canonical Operation Type Source of Truth**
|
> 4. **Customer Review Workspace v1**
|
||||||
> 5. **Platform Vocabulary Boundary Enforcement for Governed Subject Keys**
|
|
||||||
> 6. **Customer Review Workspace v1**
|
|
||||||
>
|
>
|
||||||
> Rationale: the repo already has strong baseline, findings, evidence, review, operation-run, and operator foundations. The immediate risk is not missing feature breadth, but semantic drift: provider-specific identity becoming platform truth, operation-type dual semantics, governed-subject key leakage, and control meaning being hardcoded locally instead of anchored in a canonical catalog.
|
> Rationale: the repo already has strong baseline, findings, evidence, review, operation-run, and operator foundations. With Canonical Control Catalog Foundation and Provider Boundary Hardening now specced, the immediate remaining risk is semantic drift in provider identity, operation-type dual semantics, and governed-subject key leakage.
|
||||||
|
|
||||||
|
|
||||||
|
> Platform Hardening — OperationRun UX Consistency cluster: these candidates prevent OperationRun-starting features from drifting into surface-local UX behavior. The goal is not to rebuild the Operations Hub, progress system, or notification architecture in one step. The immediate priority is to make OperationRun start UX contract-driven so new features cannot hand-roll local toasts, operation links, browser events, and queued-notification decisions independently.
|
||||||
|
|
||||||
|
### OperationRun Start UX Contract
|
||||||
|
- **Type**: hardening / architecture guardrail
|
||||||
|
- **Source**: OperationRun UX consistency analysis 2026-04-24 — `Refresh evidence` creates `tenant.evidence.snapshot.generate` runs correctly but does not consistently expose the same start UX as other OperationRun-backed flows
|
||||||
|
- **Problem**: OperationRun lifecycle state and terminal notifications are partially centralized, but the start UX is still assembled per Filament surface. Different flows independently decide local toast copy, `Open operation` links, run-enqueued browser events, dedup/no-op messaging, artifact links, and queued DB-notification behavior. This creates UX drift whenever a new feature starts, deduplicates, blocks, or links to an OperationRun.
|
||||||
|
- **Why it matters**: TenantPilot uses OperationRun as canonical execution truth. If every feature surface composes its own start UX, operators receive inconsistent signals for the same execution concept, and agent-led development will repeatedly miss one of the required pieces. Governance flows such as evidence refresh, review-pack generation, baseline capture/compare, inventory sync, backup, and restore need one reusable start contract instead of surface-local conventions.
|
||||||
|
- **Proposed direction**:
|
||||||
|
- introduce a central OperationRun start result contract that can represent new run queued, already queued, already running, already available/deduped, blocked, and failed-to-start states
|
||||||
|
- introduce a central presenter that turns the start result into consistent Filament notifications, `Open operation` links, artifact links, and browser-event decisions
|
||||||
|
- keep OperationRun detail links routed through the existing canonical link resolver rather than manually composing URLs
|
||||||
|
- keep queued DB-notifications explicit opt-in; do not globally enable queued database notifications as part of this slice
|
||||||
|
- preserve existing terminal notification behavior through the central OperationRun lifecycle
|
||||||
|
- migrate `Refresh evidence` as the first adoption and `Review pack generate` or `Create snapshot` as the second adoption
|
||||||
|
- add a guard test that prevents new hand-rolled OperationRun start-success UX in selected Filament/Livewire surfaces unless a spec explicitly allows an exception
|
||||||
|
- **Scope boundaries**:
|
||||||
|
- **In scope**: OperationRun start result contract, generic start-result presenter, `Refresh evidence` adoption, one second adoption, `Open operation` action consistency, run-enqueued browser event dispatch, queued DB-notification opt-in decision, dedup/no-op copy, guard-test coverage, and minimal spec/template guidance where appropriate
|
||||||
|
- **Out of scope**: generic progress-system redesign, `BulkOperationProgress` rename or rewrite, global queued notification policy, migration of every existing OperationRun start surface, new OperationRun types, and broad Operations Hub redesign
|
||||||
|
- **Acceptance points**:
|
||||||
|
- `Refresh evidence` distinguishes new-run from dedup/already-available outcomes in its returned start result and UI copy
|
||||||
|
- a new `tenant.evidence.snapshot.generate` run started from the UI exposes an `Open operation` action and dispatches the central run-enqueued browser event
|
||||||
|
- unchanged fingerprint / already available snapshot does not claim that a new run was queued, links the existing snapshot, and does not dispatch a run-enqueued event
|
||||||
|
- queued DB-notification remains opt-in and is not accidentally enabled globally
|
||||||
|
- terminal completed/failed/blocked notification behavior does not regress
|
||||||
|
- at least one second flow, preferably `Review pack generate`, proves the contract is not evidence-specific
|
||||||
|
- a guard test blocks new local OperationRun start-success toasts that bypass the central presenter, with explicitly documented legacy exceptions
|
||||||
|
- **Risks / open questions**:
|
||||||
|
- Over-broad static guard rules could flag legitimate non-OperationRun notifications; the first guard should be pragmatic and exception-based rather than attempting perfect static analysis
|
||||||
|
- Provider-backed flows already have a stronger gate/presenter pattern; the new generic contract should reuse or align with that pattern rather than create a competing presenter stack
|
||||||
|
- Existing manual flows should not all be migrated in this spec, otherwise the slice becomes too large
|
||||||
|
- **Dependencies**: `OperationRunService`, `OperationRunLinks`, `OperationRunUrl`, `OperationUxPresenter`, `ProviderOperationStartResultPresenter`, `OpsUxBrowserEvents`, `EvidenceSnapshotService`, `ReviewPackService`, existing OperationRun notification tests, existing link contract guard tests
|
||||||
|
- **Related specs / candidates**: Operation Run Link Contract Enforcement (Spec 232), Operation Run Active-State Visibility & Stale Escalation (Spec 233), Provider-Backed Action Preflight and Dispatch Gate Unification (Spec 216), OperationRun Notification Lifecycle, Generic Active Run Surface, OperationRun Startsurface Migration
|
||||||
|
- **Strategic sequencing**: First item in the OperationRun UX Consistency cluster. It should land before additional large evidence, review, baseline, or governance operations are added, so new OperationRun-backed features inherit the same start contract from the beginning.
|
||||||
|
- **Priority**: high
|
||||||
|
|
||||||
|
### Generic Active Run Surface
|
||||||
|
- **Type**: hardening / monitoring UX
|
||||||
|
- **Source**: OperationRun UX consistency analysis 2026-04-24 — current active-run surface is historically named `BulkOperationProgress` and does not clearly separate true progress metrics from active-run visibility
|
||||||
|
- **Problem**: The global run/progress surface is historically shaped around bulk operations, while the product now has many OperationRun types. Some runs have meaningful progress metrics, while others only have queued/running/completed state. The current surface can be refreshed by run-enqueued browser events, but not every run-starting flow dispatches those events, and the widget does not consistently model whether a run is progress-capable or active-status-only.
|
||||||
|
- **Why it matters**: Operators need immediate confidence that a started operation is visible and still active, but the UI must not imply fake percent progress for jobs that do not expose real progress. As OperationRun becomes the execution truth for evidence, reviews, baselines, inventory, backup, restore, and provider checks, the active-run surface must communicate the right level of detail without noise.
|
||||||
|
- **Proposed direction**:
|
||||||
|
- classify OperationRun types by display capability: progress-capable, active-status-only, or terminal-only
|
||||||
|
- decide whether `BulkOperationProgress` remains bulk-specific, is wrapped by a generic active-run surface, or is gradually renamed/refactored
|
||||||
|
- display progress bars only for runs with real progress counters
|
||||||
|
- display simple queued/running indicators for short or active-status-only runs
|
||||||
|
- ensure run-enqueued events from the OperationRun Start UX Contract refresh the active-run surface reliably
|
||||||
|
- keep tenant/workspace filtering explicit so the surface never leaks cross-context runs
|
||||||
|
- **Scope boundaries**:
|
||||||
|
- **In scope**: active-run surface semantics, progress-capability classification, polling rules, browser-event refresh behavior, active-status-only rendering, and tenant/workspace filtering
|
||||||
|
- **Out of scope**: OperationRun start contract itself, DB-notification lifecycle policy, external alerting, and migration of all start surfaces
|
||||||
|
- **Acceptance points**:
|
||||||
|
- every displayed run type is classified as progress-capable or active-status-only
|
||||||
|
- runs without real progress metrics are not rendered with misleading percent/progress values
|
||||||
|
- active run polling remains quiet when no relevant active runs exist
|
||||||
|
- run-enqueued events refresh the surface consistently for relevant active runs
|
||||||
|
- tenant/workspace scoping is enforced in queries and tests
|
||||||
|
- **Dependencies**: OperationRun Start UX Contract, `BulkOperationProgress`, `ActiveRuns`, `OpsUxBrowserEvents`, `OperationRun`, `OperationRunType`
|
||||||
|
- **Related specs / candidates**: Operation Run Active-State Visibility & Stale Escalation (Spec 233), OperationRun Start UX Contract, OperationRun Startsurface Migration
|
||||||
|
- **Strategic sequencing**: Second item in the OperationRun UX Consistency cluster, after the start contract establishes when run-enqueued events should be emitted.
|
||||||
|
- **Priority**: medium-high
|
||||||
|
|
||||||
|
### OperationRun Notification Lifecycle
|
||||||
|
- **Type**: hardening / notification policy
|
||||||
|
- **Source**: OperationRun UX consistency analysis 2026-04-24 — queued DB-notifications exist but are not broadly used, while terminal notifications are handled centrally through the OperationRun lifecycle
|
||||||
|
- **Problem**: OperationRun notifications currently mix clear terminal lifecycle behavior with unclear queued/running policy. Queued DB-notifications are technically available but intentionally opt-in and rarely used in app code. Terminal completed/failed/blocked messaging is more centralized, but failed and blocked states are still communicated through a shared terminal notification path rather than a clearly documented lifecycle policy.
|
||||||
|
- **Why it matters**: Persisted DB-notifications can quickly become noisy in an MSP/operator product. At the same time, long-running or critical operations may need durable notifications beyond local toasts. The product needs an explicit policy for when a run deserves a persisted queued/running/terminal notification, who receives it, and how duplicates are prevented.
|
||||||
|
- **Proposed direction**:
|
||||||
|
- define a lifecycle notification policy for queued, running, completed, failed, blocked, and any relevant stale/canceled states
|
||||||
|
- keep queued DB-notifications opt-in unless the spec defines precise criteria for enabling them
|
||||||
|
- clarify whether terminal states should continue through a shared `OperationRunCompleted` channel or split failed/blocked into explicit notification classes
|
||||||
|
- define recipient rules: initiator-only, workspace members, tenant members, or capability-based recipients
|
||||||
|
- define duplicate-prevention and one-terminal-notification guarantees
|
||||||
|
- decide whether Filament database notifications should poll or remain manually refreshed
|
||||||
|
- **Scope boundaries**:
|
||||||
|
- **In scope**: DB-notification lifecycle policy, recipient policy, queued opt-in criteria, terminal notification semantics, duplicate prevention, and panel polling decision
|
||||||
|
- **Out of scope**: Teams/email alert routing, findings escalation rules, external webhook/PSA notifications, generic progress surface, and the OperationRun start-result contract itself
|
||||||
|
- **Acceptance points**:
|
||||||
|
- lifecycle notification policy is explicit for queued/running/completed/failed/blocked states
|
||||||
|
- queued DB-notifications remain controlled and are not globally enabled accidentally
|
||||||
|
- terminal notifications are emitted exactly once per relevant run lifecycle
|
||||||
|
- failed and blocked terminal messaging is clear to operators
|
||||||
|
- recipient selection is documented and tested
|
||||||
|
- Filament DB-notification polling is intentionally enabled or intentionally disabled with tests
|
||||||
|
- **Dependencies**: OperationRun Start UX Contract, `OperationRunService`, `OperationRunQueued`, `OperationRunCompleted`, `OperationUxPresenter`, Filament panel providers
|
||||||
|
- **Related specs / candidates**: Findings Notifications & Escalation v1 (Spec 224), Findings Notification Presentation Convergence (Spec 230), OperationRun Start UX Contract, Generic Active Run Surface
|
||||||
|
- **Strategic sequencing**: Third item in the OperationRun UX Consistency cluster. It should follow the start contract so local start toasts and persisted DB-notifications remain clearly separated.
|
||||||
|
- **Priority**: medium
|
||||||
|
|
||||||
|
### OperationRun Startsurface Migration
|
||||||
|
- **Type**: hardening / migration slice
|
||||||
|
- **Source**: OperationRun UX consistency analysis 2026-04-24 — many existing OperationRun-starting surfaces already implement partial local patterns and should be migrated gradually after the shared contract exists
|
||||||
|
- **Problem**: Even after a central OperationRun Start UX Contract exists, older start surfaces will continue to contain manual toast, link, dedup, browser-event, and artifact-link behavior. If they remain indefinitely, they become examples for future features and keep the UX inconsistent.
|
||||||
|
- **Why it matters**: The contract prevents new drift, but existing drift still affects daily operator experience. A controlled migration strand lets the product converge without turning the initial contract spec into an oversized refactor.
|
||||||
|
- **Proposed direction**:
|
||||||
|
- inventory OperationRun-starting surfaces and classify them as migrated, legacy/manual allowed, out of scope, or not actually OperationRun-starting
|
||||||
|
- reduce guard-test exceptions over time
|
||||||
|
- migrate flows in priority order: remaining Evidence/Review Pack surfaces, Baseline Capture, Baseline Compare, Inventory Sync, Backup Schedule, Backup Set bulk operations, Restore Execute, Provider Operations if still needed
|
||||||
|
- for each migrated flow, route toast, `Open operation`, artifact link, browser event, and queued DB-notification decision through the central contract
|
||||||
|
- preserve existing operation semantics while removing surface-local UX composition
|
||||||
|
- **Scope boundaries**:
|
||||||
|
- **In scope**: migration of existing OperationRun start surfaces to the PH.1 contract, reduction of guard exceptions, tests per migrated flow, and documentation of any permanent exceptions
|
||||||
|
- **Out of scope**: new OperationRun architecture, generic active-run surface design, notification lifecycle redesign, broad UI redesign, and changing job execution semantics
|
||||||
|
- **Acceptance points**:
|
||||||
|
- an inventory of known OperationRun-starting surfaces exists and is kept near the guard test or migration documentation
|
||||||
|
- migrated surfaces no longer hand-roll the combination of start toast, operation link, and browser event
|
||||||
|
- dedup/already-running/blocked states are represented through the central contract where applicable
|
||||||
|
- guard-test exceptions are reduced or justified with spec references
|
||||||
|
- each migrated flow has new-run and dedup/already-running tests where applicable
|
||||||
|
- **Dependencies**: OperationRun Start UX Contract; optionally Generic Active Run Surface and OperationRun Notification Lifecycle for later migration waves
|
||||||
|
- **Related specs / candidates**: OperationRun Start UX Contract, Generic Active Run Surface, OperationRun Notification Lifecycle, Provider-Backed Action Preflight and Dispatch Gate Unification (Spec 216)
|
||||||
|
- **Strategic sequencing**: Fourth item in the OperationRun UX Consistency cluster. It should not block PH.1 but should be used to retire manual legacy patterns incrementally.
|
||||||
|
- **Priority**: medium
|
||||||
|
|
||||||
|
> Recommended sequence for this cluster:
|
||||||
|
> 1. **OperationRun Start UX Contract**
|
||||||
|
> 2. **Generic Active Run Surface**
|
||||||
|
> 3. **OperationRun Notification Lifecycle**
|
||||||
|
> 4. **OperationRun Startsurface Migration**
|
||||||
|
>
|
||||||
|
> Why this order: first establish the mandatory start contract and guardrails, then clarify active-run visibility, then define durable notification policy, and only then migrate remaining legacy/manual surfaces in controlled waves.
|
||||||
|
|
||||||
> Provider-boundary / future-provider portability cluster: these candidates are intentionally **not** a multi-cloud execution program. The goal is to keep Microsoft-first hotspots small at shared platform seams so a later second provider remains a bounded follow-up instead of a rewrite. Current product truth stays Microsoft-first; these candidates only harden where provider-specific semantics are at risk of becoming platform-core truth.
|
> Provider-boundary / future-provider portability cluster: these candidates are intentionally **not** a multi-cloud execution program. The goal is to keep Microsoft-first hotspots small at shared platform seams so a later second provider remains a bounded follow-up instead of a rewrite. Current product truth stays Microsoft-first; these candidates only harden where provider-specific semantics are at risk of becoming platform-core truth.
|
||||||
|
|
||||||
### Provider Boundary Hardening
|
|
||||||
- **Type**: hardening / architecture boundary
|
|
||||||
- **Source**: provider portability audit 2026-04-23 — current foundation judged provider-agnostic enough with bounded Microsoft hotspots
|
|
||||||
- **Problem**: TenantPilot already has a generic-looking provider layer in some places, but shared seams such as provider gateways, operation orchestration, and supporting contracts can still inherit Graph- or Microsoft-shaped semantics by default. Without a bounded hardening pass, new work can keep treating provider-specific contracts as if they were platform-core truth.
|
|
||||||
- **Why it matters**: The immediate risk is not "no AWS/GCP support today". The real risk is silent deepening of provider coupling in shared contracts, which would make any later second-provider work expensive and cross-cutting.
|
|
||||||
- **Proposed direction**:
|
|
||||||
- classify touched seams as **provider-owned** or **platform-core**
|
|
||||||
- keep provider-specific contracts behind adapters or explicitly provider-owned services
|
|
||||||
- harden shared platform contracts around neutral concepts only where current workflows already cross provider-specific boundaries
|
|
||||||
- add review/test guardrails that prevent new platform-core Graph leakage
|
|
||||||
- **Scope boundaries**:
|
|
||||||
- **In scope**: contract-boundary audit, targeted extractions or normalizations at shared seams, review/test guardrails for new coupling
|
|
||||||
- **Out of scope**: full multi-provider runtime, AWS/GCP adapters, speculative provider registries, broad rebranding away from Microsoft/Intune vocabulary everywhere
|
|
||||||
- **Dependencies**: current `ProviderGateway` / `GraphClientInterface` seam, provider-boundary constitution guardrails
|
|
||||||
- **Priority**: high
|
|
||||||
|
|
||||||
### Provider Identity & Target Scope Neutrality
|
### Provider Identity & Target Scope Neutrality
|
||||||
- **Type**: hardening / identity boundary
|
- **Type**: hardening / identity boundary
|
||||||
- **Source**: provider portability audit 2026-04-23 — `ProviderConnection` is generic by name but still carries Entra-shaped identity and target-scope semantics
|
- **Source**: provider portability audit 2026-04-23 — `ProviderConnection` is generic by name but still carries Entra-shaped identity and target-scope semantics
|
||||||
@ -157,13 +262,12 @@ ### Provider Surface Vocabulary & Descriptor Cleanup
|
|||||||
- **Priority**: medium
|
- **Priority**: medium
|
||||||
|
|
||||||
> Recommended sequence for this cluster:
|
> Recommended sequence for this cluster:
|
||||||
> 1. **Provider Boundary Hardening**
|
> 1. **Provider Identity & Target Scope Neutrality**
|
||||||
> 2. **Provider Identity & Target Scope Neutrality**
|
> 2. **Governance Subject Taxonomy Decoupling**
|
||||||
> 3. **Governance Subject Taxonomy Decoupling**
|
> 3. **Compare Strategy Boundary Hardening**
|
||||||
> 4. **Compare Strategy Boundary Hardening**
|
> 4. **Provider Surface Vocabulary & Descriptor Cleanup**
|
||||||
> 5. **Provider Surface Vocabulary & Descriptor Cleanup**
|
|
||||||
>
|
>
|
||||||
> Why this order: the first three keep the foundation from becoming more provider-shaped. Compare and UI vocabulary cleanup remain valuable, but they are safer once shared contracts, identity scope, and governance taxonomy stop deepening Microsoft coupling.
|
> Why this order: Provider Boundary Hardening is now specced as the first bounded anti-drift pass. The remaining items keep that foundation from being weakened again at identity scope, governance taxonomy, compare fallback, and shared provider-facing vocabulary.
|
||||||
|
|
||||||
### Workspace Access Context and Navigation Cost Hardening
|
### Workspace Access Context and Navigation Cost Hardening
|
||||||
- **Type**: hardening
|
- **Type**: hardening
|
||||||
|
|||||||
@ -4,7 +4,7 @@ # Product Standards
|
|||||||
> Specs reference these standards; they do not redefine them.
|
> Specs reference these standards; they do not redefine them.
|
||||||
> Guard tests enforce critical constraints automatically.
|
> Guard tests enforce critical constraints automatically.
|
||||||
|
|
||||||
**Last reviewed**: 2026-04-12
|
**Last reviewed**: 2026-04-24
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ ## Related Docs
|
|||||||
|
|
||||||
| Document | Location | Purpose |
|
| Document | Location | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Constitution | `.specify/memory/constitution.md` | Permanent principles (PROP-001, BLOAT-001, UI-CONST-001, DECIDE-001, UI-SURF-001, ACTSURF-001, UI-HARD-001, UI-EX-001, HDR-001, OPSURF-001, UI-FIL-001, UX-001, Action Surface Contract, RBAC-UX) |
|
| Constitution | `.specify/memory/constitution.md` | Permanent principles (PROP-001, BLOAT-001, OPS-UX-START-001, UI-CONST-001, DECIDE-001, UI-SURF-001, ACTSURF-001, UI-HARD-001, UI-EX-001, HDR-001, OPSURF-001, UI-FIL-001, UX-001, Action Surface Contract, RBAC-UX) |
|
||||||
| Product Principles | `docs/product/principles.md` | High-level product decisions |
|
| Product Principles | `docs/product/principles.md` | High-level product decisions |
|
||||||
| Table Rollout Audit | `docs/ui/filament-table-standard.md` | Rollout inventory and implementation state from Spec 125 |
|
| Table Rollout Audit | `docs/ui/filament-table-standard.md` | Rollout inventory and implementation state from Spec 125 |
|
||||||
| Action Surface Contract | `docs/ui/action-surface-contract.md` | Original action surface reference (now governed by this standard) |
|
| Action Surface Contract | `docs/ui/action-surface-contract.md` | Original action surface reference (now governed by this standard) |
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
# Specification Quality Checklist: Provider Boundary Hardening
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-04-24
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Initial draft created from the prioritized candidate sequence in [docs/product/spec-candidates.md](../../../docs/product/spec-candidates.md) and [docs/product/roadmap.md](../../../docs/product/roadmap.md).
|
||||||
|
- Repo-required constitution and validation sections remain intentionally technical, but the feature scope, scenarios, requirements, and success criteria stay solution-agnostic.
|
||||||
@ -0,0 +1,207 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Provider Boundary Hardening Logical Contract
|
||||||
|
version: 0.1.0
|
||||||
|
description: |
|
||||||
|
Logical internal contract for the first provider-boundary hardening slice.
|
||||||
|
It describes shared shapes for listing seam ownership, resolving operation
|
||||||
|
definition versus provider binding, and evaluating touched boundary changes.
|
||||||
|
It is not a commitment to expose public HTTP routes.
|
||||||
|
paths:
|
||||||
|
/logical/provider-boundaries/seams:
|
||||||
|
get:
|
||||||
|
summary: List the first-slice provider boundary seam ownership catalog
|
||||||
|
operationId: listProviderBoundarySeams
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Boundary seam catalog
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
seams:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ProviderBoundarySeam'
|
||||||
|
required:
|
||||||
|
- seams
|
||||||
|
/logical/provider-boundaries/operations/{operationType}:
|
||||||
|
get:
|
||||||
|
summary: Read platform-core operation definition and current provider binding
|
||||||
|
operationId: getProviderBoundaryOperation
|
||||||
|
parameters:
|
||||||
|
- name: operationType
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Operation definition and binding
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ProviderOperationBoundaryResponse'
|
||||||
|
/logical/provider-boundaries/evaluate:
|
||||||
|
post:
|
||||||
|
summary: Evaluate whether a touched change respects the declared boundary
|
||||||
|
operationId: evaluateProviderBoundaryChange
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ProviderBoundaryEvaluationRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Boundary evaluation outcome
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ProviderBoundaryCheckResult'
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
ProviderBoundaryOwner:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- provider_owned
|
||||||
|
- platform_core
|
||||||
|
ProviderBoundarySeam:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
seam_key:
|
||||||
|
type: string
|
||||||
|
owner:
|
||||||
|
$ref: '#/components/schemas/ProviderBoundaryOwner'
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
implementation_paths:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
neutral_terms:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
retained_provider_semantics:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
follow_up_action:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- none
|
||||||
|
- document-in-feature
|
||||||
|
- follow-up-spec
|
||||||
|
required:
|
||||||
|
- seam_key
|
||||||
|
- owner
|
||||||
|
- description
|
||||||
|
- implementation_paths
|
||||||
|
- neutral_terms
|
||||||
|
- retained_provider_semantics
|
||||||
|
- follow_up_action
|
||||||
|
ProviderOperationDefinition:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
operation_type:
|
||||||
|
type: string
|
||||||
|
module_key:
|
||||||
|
type: string
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
required_capability:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- operation_type
|
||||||
|
- module_key
|
||||||
|
- label
|
||||||
|
- required_capability
|
||||||
|
ProviderOperationBinding:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
operation_type:
|
||||||
|
type: string
|
||||||
|
provider:
|
||||||
|
type: string
|
||||||
|
binding_status:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- active
|
||||||
|
- unsupported
|
||||||
|
handler_notes:
|
||||||
|
type: string
|
||||||
|
exception_notes:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- operation_type
|
||||||
|
- provider
|
||||||
|
- binding_status
|
||||||
|
ProviderOperationBoundaryResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
definition:
|
||||||
|
$ref: '#/components/schemas/ProviderOperationDefinition'
|
||||||
|
binding:
|
||||||
|
$ref: '#/components/schemas/ProviderOperationBinding'
|
||||||
|
required:
|
||||||
|
- definition
|
||||||
|
- binding
|
||||||
|
ProviderBoundaryEvaluationRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
seam_key:
|
||||||
|
type: string
|
||||||
|
file_path:
|
||||||
|
type: string
|
||||||
|
proposed_owner:
|
||||||
|
$ref: '#/components/schemas/ProviderBoundaryOwner'
|
||||||
|
provider_specific_terms:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
introduces_new_binding:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- seam_key
|
||||||
|
- file_path
|
||||||
|
- proposed_owner
|
||||||
|
- provider_specific_terms
|
||||||
|
- introduces_new_binding
|
||||||
|
ProviderBoundaryCheckResult:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- allowed
|
||||||
|
- review_required
|
||||||
|
- blocked
|
||||||
|
seam_key:
|
||||||
|
type: string
|
||||||
|
file_path:
|
||||||
|
type: string
|
||||||
|
violation_code:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- none
|
||||||
|
- platform_core_provider_leak
|
||||||
|
- undeclared_exception
|
||||||
|
- missing_provider_binding
|
||||||
|
- provider_binding_as_primary_truth
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
suggested_follow_up:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- none
|
||||||
|
- document-in-feature
|
||||||
|
- follow-up-spec
|
||||||
|
required:
|
||||||
|
- status
|
||||||
|
- seam_key
|
||||||
|
- file_path
|
||||||
|
- violation_code
|
||||||
|
- message
|
||||||
|
- suggested_follow_up
|
||||||
115
specs/237-provider-boundary-hardening/data-model.md
Normal file
115
specs/237-provider-boundary-hardening/data-model.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# Data Model: Provider Boundary Hardening
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The first slice introduces one explicit seam-ownership catalog with binary ownership plus documented exception metadata, one split between shared operation definition and provider binding, and one deterministic guard result shape. No new database persistence is introduced.
|
||||||
|
|
||||||
|
## Entity: ProviderBoundarySeam
|
||||||
|
|
||||||
|
- **Purpose**: Declares whether a touched seam is provider-owned or platform-core, and records any documented current-release exception metadata that remains temporarily allowed.
|
||||||
|
- **Identity**:
|
||||||
|
- `seam_key` — stable catalog key such as `provider.gateway_runtime`, `provider.identity_resolution`, or `provider.operation_registry`
|
||||||
|
- **Core fields**:
|
||||||
|
- `owner`
|
||||||
|
- `description`
|
||||||
|
- `implementation_paths[]`
|
||||||
|
- `neutral_terms[]`
|
||||||
|
- `retained_provider_semantics[]`
|
||||||
|
- `follow_up_action`
|
||||||
|
- **Owner values**:
|
||||||
|
- `provider_owned`
|
||||||
|
- `platform_core`
|
||||||
|
- **Validation rules**:
|
||||||
|
- Every first-slice seam must have exactly one owner classification.
|
||||||
|
- `platform_core` seams must list the neutral platform terms they preserve.
|
||||||
|
- Any seam that retains provider-specific semantics must list them in `retained_provider_semantics[]` and pair them with an explicit `follow_up_action`.
|
||||||
|
- `implementation_paths[]` must reference real code paths used by tests and review guardrails.
|
||||||
|
|
||||||
|
### Authoritative first-slice seam inventory
|
||||||
|
|
||||||
|
- `provider.gateway_runtime` — `ProviderGateway.php`, `MicrosoftGraphOptionsResolver.php`
|
||||||
|
- `provider.identity_resolution` — `ProviderIdentityResolution.php`, `ProviderIdentityResolver.php`, `PlatformProviderIdentityResolver.php`
|
||||||
|
- `provider.connection_resolution` — `ProviderConnectionResolver.php`, `ProviderConnectionResolution.php`
|
||||||
|
- `provider.operation_registry` — `ProviderOperationRegistry.php`
|
||||||
|
- `provider.operation_start_gate` — `ProviderOperationStartGate.php`
|
||||||
|
|
||||||
|
## Entity: ProviderOperationDefinition
|
||||||
|
|
||||||
|
- **Purpose**: Represents the platform-core definition of a provider-backed operation without silently embedding provider truth.
|
||||||
|
- **Identity**:
|
||||||
|
- `operation_type`
|
||||||
|
- **Core fields**:
|
||||||
|
- `module_key`
|
||||||
|
- `label`
|
||||||
|
- `required_capability`
|
||||||
|
- **Validation rules**:
|
||||||
|
- The definition must not require a provider field as its primary identity.
|
||||||
|
- Labels and module keys remain shared orchestration truth and do not encode Microsoft-specific vocabulary by default.
|
||||||
|
- Operation type values remain unchanged in this slice.
|
||||||
|
|
||||||
|
## Entity: ProviderOperationBinding
|
||||||
|
|
||||||
|
- **Purpose**: Connects one shared operation definition to the current provider-owned runtime behavior.
|
||||||
|
- **Fields**:
|
||||||
|
- `operation_type`
|
||||||
|
- `provider`
|
||||||
|
- `binding_status`
|
||||||
|
- `handler_notes`
|
||||||
|
- `exception_notes`
|
||||||
|
- **Binding status values**:
|
||||||
|
- `active`
|
||||||
|
- `unsupported`
|
||||||
|
- **Validation rules**:
|
||||||
|
- Every binding must reference an existing `operation_type`.
|
||||||
|
- Missing binding is explicit and must not fall back silently to Microsoft behavior.
|
||||||
|
- Provider-specific notes remain secondary to the platform-core operation definition.
|
||||||
|
|
||||||
|
## Entity: ProviderBoundaryCheckResult
|
||||||
|
|
||||||
|
- **Purpose**: Shared deterministic result used by tests and review guardrails.
|
||||||
|
- **Fields**:
|
||||||
|
- `status`
|
||||||
|
- `seam_key`
|
||||||
|
- `file_path`
|
||||||
|
- `violation_code`
|
||||||
|
- `message`
|
||||||
|
- `suggested_follow_up`
|
||||||
|
- **Status values**:
|
||||||
|
- `allowed`
|
||||||
|
- `review_required`
|
||||||
|
- `blocked`
|
||||||
|
- **Violation code examples**:
|
||||||
|
- `platform_core_provider_leak`
|
||||||
|
- `undeclared_exception`
|
||||||
|
- `missing_provider_binding`
|
||||||
|
- `provider_binding_as_primary_truth`
|
||||||
|
- **Validation rules**:
|
||||||
|
- `allowed` means the touched seam matches its declared ownership.
|
||||||
|
- `review_required` means the seam has documented exception metadata or needs explicit follow-up.
|
||||||
|
- `blocked` means the touched change violates the seam ownership or provider-binding rules.
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
- One `ProviderBoundarySeam` may cover multiple implementation paths.
|
||||||
|
- One `ProviderOperationDefinition` may have one current `ProviderOperationBinding` in the first slice because only Microsoft runtime support exists today.
|
||||||
|
- One `ProviderBoundaryCheckResult` references exactly one seam evaluation, but a review may emit multiple results across multiple touched files.
|
||||||
|
|
||||||
|
## Lifecycle
|
||||||
|
|
||||||
|
### Boundary seam lifecycle
|
||||||
|
|
||||||
|
- `provider_owned`: provider-specific semantics are allowed inside the seam.
|
||||||
|
- `platform_core`: provider-specific semantics are blocked unless explicitly extracted.
|
||||||
|
- `platform_core` with documented exception metadata: named provider-specific details remain temporarily allowed only while the seam records the retained semantics and explicit follow-up action.
|
||||||
|
|
||||||
|
### Operation binding lifecycle
|
||||||
|
|
||||||
|
- `active`: current provider runtime is explicitly supported for the operation.
|
||||||
|
- `unsupported`: the operation definition exists, but the current seam must fail explicitly instead of inheriting default provider behavior.
|
||||||
|
|
||||||
|
## Rollout Model
|
||||||
|
|
||||||
|
- The first slice keeps the catalog in code, not in the database.
|
||||||
|
- Existing Microsoft-backed runtime behavior remains the only shipped provider binding.
|
||||||
|
- Shared identity resolution and operation registry cleanup happen before any persistence or UI neutrality changes.
|
||||||
|
- Deeper target-scope and provider-connection neutrality remain follow-up work.
|
||||||
253
specs/237-provider-boundary-hardening/plan.md
Normal file
253
specs/237-provider-boundary-hardening/plan.md
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
# Implementation Plan: Provider Boundary Hardening
|
||||||
|
|
||||||
|
**Branch**: `237-provider-boundary-hardening` | **Date**: 2026-04-24 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/237-provider-boundary-hardening/spec.md`
|
||||||
|
|
||||||
|
**Note**: This plan keeps the slice intentionally narrow. It classifies the first high-risk shared provider seams, removes Graph-shaped request building from shared identity resolution, makes provider binding explicit in the shared operation registry path, and adds focused guardrails without introducing a second-provider runtime, new persistence, or new operator-facing surfaces.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add a config-seeded provider-boundary catalog plus a small `App\Support\Providers\Boundary` helper layer to classify the first hot seams as `provider_owned` or `platform_core`, with documented current-release exception metadata where needed. The authoritative first-slice seam inventory is locked to `provider.gateway_runtime`, `provider.identity_resolution`, `provider.connection_resolution`, `provider.operation_registry`, and `provider.operation_start_gate`. The implementation will harden two concrete hotspots: first, move Graph request-option shaping out of `ProviderIdentityResolution` and keep it inside provider-owned seams such as `ProviderGateway` and `MicrosoftGraphOptionsResolver`; second, split `ProviderOperationRegistry` into platform-core operation metadata plus explicit provider binding metadata so `ProviderOperationStartGate` no longer treats `microsoft` as silent platform-default truth. Existing Microsoft-backed flows stay intact, `entra_tenant_id` and platform app identity remain documented current-release exceptions for the follow-up identity-neutrality slice, and the boundary is enforced through focused unit plus feature guard tests.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4
|
||||||
|
**Primary Dependencies**: existing provider seams under `App\Services\Providers` and `App\Services\Graph`, especially `ProviderGateway`, `ProviderIdentityResolver`, `ProviderIdentityResolution`, `PlatformProviderIdentityResolver`, `ProviderConnectionResolver`, `ProviderConnectionResolution`, `MicrosoftGraphOptionsResolver`, `ProviderOperationRegistry`, `ProviderOperationStartGate`, `GraphClientInterface`, Pest v4
|
||||||
|
**Storage**: Existing PostgreSQL tables such as `provider_connections` and `operation_runs`; one new in-repo config catalog for provider-boundary ownership; no new database tables
|
||||||
|
**Testing**: Pest v4 unit and focused feature tests through Laravel Sail
|
||||||
|
**Validation Lanes**: `fast-feedback`, `confidence`
|
||||||
|
**Target Platform**: Laravel admin web application running in Sail on the existing `/admin`, `/admin/t/{tenant}`, and provider-backed operation surfaces
|
||||||
|
**Project Type**: Monorepo with one Laravel runtime in `apps/platform` and spec artifacts at repository root
|
||||||
|
**Performance Goals**: Keep boundary evaluation deterministic and in-process, add no outbound call before existing provider-owned execution seams, and preserve current provider-backed runtime performance on supported Microsoft flows
|
||||||
|
**Constraints**: No new provider runtime, no broad provider marketplace abstraction, no schema or route redesign, no operation-type renaming, no new UI surface, no new Graph contract path, and no silent Microsoft fallback on touched shared seams
|
||||||
|
**Scale/Scope**: One config-backed boundary catalog, one small boundary support namespace, one shared identity-resolution cleanup, one shared operation-registry cleanup, and focused unit plus feature guard coverage
|
||||||
|
|
||||||
|
## Filament v5 Implementation Contract
|
||||||
|
|
||||||
|
- **Livewire v4.0+ compliance**: Preserved. This slice changes shared services, value objects, and guardrails only and introduces no legacy Livewire patterns.
|
||||||
|
- **Provider registration location**: Unchanged. Panel providers remain registered in `apps/platform/bootstrap/providers.php`.
|
||||||
|
- **Global search coverage**: No new Filament Resource or Page is added, and no existing global-search posture changes in this slice. Provider connection surfaces remain on their current search posture.
|
||||||
|
- **Destructive actions**: No destructive action is added or changed. This slice does not introduce new Filament actions.
|
||||||
|
- **Asset strategy**: No new assets are planned. Deployment expectations remain unchanged, including `cd apps/platform && php artisan filament:assets` only when later UI work introduces registered assets.
|
||||||
|
- **Testing plan**: Prove the slice with focused Pest unit coverage for seam classification and registry behavior plus focused feature coverage for current Microsoft runtime preservation, unsupported-path behavior, and boundary guardrails.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: workflow-only guardrail change
|
||||||
|
- **Native vs custom classification summary**: `N/A`
|
||||||
|
- **Shared-family relevance**: provider-backed execution seams, provider connection runtime semantics, shared architecture guards
|
||||||
|
- **State layers in scope**: none
|
||||||
|
- **Handling modes by drift class or surface**: `review-mandatory`
|
||||||
|
- **Repository-signal treatment**: `review-mandatory`
|
||||||
|
- **Special surface test profiles**: `N/A`
|
||||||
|
- **Required tests or manual smoke**: `functional-core`, `state-contract`
|
||||||
|
- **Exception path and spread control**: one named current-release exception boundary for Microsoft-specific target-scope and platform app identity semantics that remain until the follow-up identity-neutrality spec
|
||||||
|
- **Active feature PR close-out entry**: `Guardrail`
|
||||||
|
|
||||||
|
## Shared Pattern & System Fit
|
||||||
|
|
||||||
|
- **Cross-cutting feature marker**: yes
|
||||||
|
- **Systems touched**: provider gateway/runtime access, provider identity resolution, provider connection validation, provider-backed operation registry and start gate, provider-owned reason and next-step semantics, and adjacent feature guard patterns
|
||||||
|
- **Shared abstractions reused**: existing provider services, existing `GraphClientInterface` contract, existing `ProviderOperationStartGate`, existing feature-guard patterns under `tests/Feature/Guards` such as `NoLegacyTenantGraphOptionsTest.php` and `NoLegacyTenantProviderFallbackTest.php`, and existing provider unit suites
|
||||||
|
- **New abstraction introduced? why?**: yes. A small boundary catalog and boundary descriptor layer are required because prose and generic class names alone are not machine-checkable and have not prevented drift.
|
||||||
|
- **Why the existing abstraction was sufficient or insufficient**: existing provider seams are sufficient as extension points, but insufficiently explicit about ownership. `ProviderIdentityResolution::graphOptions()` and `ProviderOperationRegistry` currently mix provider-specific semantics into shared paths.
|
||||||
|
- **Bounded deviation / spread control**: the only allowed retained deviation is the documented Microsoft-first identity/target-scope exception on existing provider connection data until the follow-up identity-neutrality slice lands
|
||||||
|
|
||||||
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Provider-owned seams**: `GraphClientInterface` implementations, `ProviderGateway`, `MicrosoftGraphOptionsResolver`, and Intune-specific service calls that intentionally execute Microsoft behavior
|
||||||
|
- **Platform-core seams**: provider-boundary catalog, `ProviderIdentityResolver`, `ProviderIdentityResolution`, `ProviderConnectionResolver`, `ProviderConnectionResolution`, `ProviderOperationRegistry` core operation definition path, `ProviderOperationStartGate` shared orchestration decisions
|
||||||
|
- **Authoritative first-slice seam inventory**:
|
||||||
|
- `provider.gateway_runtime` -> `ProviderGateway.php`, `MicrosoftGraphOptionsResolver.php`
|
||||||
|
- `provider.identity_resolution` -> `ProviderIdentityResolution.php`, `ProviderIdentityResolver.php`, `PlatformProviderIdentityResolver.php`
|
||||||
|
- `provider.connection_resolution` -> `ProviderConnectionResolver.php`, `ProviderConnectionResolution.php`
|
||||||
|
- `provider.operation_registry` -> `ProviderOperationRegistry.php`
|
||||||
|
- `provider.operation_start_gate` -> `ProviderOperationStartGate.php`
|
||||||
|
- **Neutral platform terms / contracts preserved**: provider, provider connection, target scope, operation type, operation module, required capability, provider binding, unsupported provider behavior
|
||||||
|
- **Retained provider-specific semantics and why**: `entra_tenant_id`, platform app credential config, redirect callback details, and Microsoft Graph request-option keys remain current-release Microsoft semantics because they are still needed for the only shipped provider runtime today
|
||||||
|
- **Bounded extraction or follow-up path**: `follow-up-spec` for Provider Identity & Target Scope Neutrality; this feature documents and bounds the remaining identity-shaped hotspot instead of solving schema and UI neutrality here
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Passed before Phase 0 research. Re-check after Phase 1 design: still passed with one config-backed seam catalog, one bounded runtime extraction, and no new persistence or operator surface.*
|
||||||
|
|
||||||
|
| Gate | Status | Plan Notes |
|
||||||
|
|------|--------|------------|
|
||||||
|
| Inventory-first / read-write separation | PASS | The slice hardens contracts and runtime boundaries only. No new write path, preview flow, or operator mutation surface is introduced. |
|
||||||
|
| Single Graph contract path / no inline remote work | PASS | Existing Graph calls remain behind `GraphClientInterface`. The slice only relocates Graph option shaping to provider-owned seams and adds no new contract bypass. |
|
||||||
|
| RBAC, workspace isolation, tenant isolation | PASS | No new route, capability, or authorization plane is introduced. Existing provider-backed workflows keep their current tenant and workspace guards. |
|
||||||
|
| Run observability / Ops-UX lifecycle | PASS | The feature may touch `ProviderOperationStartGate`, but it does not create a new `OperationRun` type or change start-surface UX semantics. Existing service-owned run lifecycle rules remain intact. |
|
||||||
|
| Shared pattern first | PASS | The implementation reuses existing provider services and the existing guard-test pattern instead of creating a parallel portability framework. |
|
||||||
|
| Proportionality / no premature abstraction | PASS | One boundary catalog plus small descriptors are the narrowest machine-checkable source of truth for multiple real seams. No plugin system, provider marketplace, or second runtime is introduced. |
|
||||||
|
| Persisted truth / behavioral state | PASS | No new table or persisted lifecycle is added. One or two new provider-boundary reason codes may be introduced only if explicit unsupported-path behavior needs stable runtime semantics. |
|
||||||
|
| Provider boundary | PASS | The plan explicitly separates provider-owned seams from platform-core seams and records one bounded Microsoft-first exception path. |
|
||||||
|
| Filament v5 / Livewire v4 contract | PASS | No new Filament surface, action, or global-search behavior is introduced. Provider registration remains in `bootstrap/providers.php`. |
|
||||||
|
| Test governance | PASS | Coverage stays in focused unit plus feature lanes with no browser or heavy-governance expansion. |
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: `Unit` for seam classification, registry split semantics, and retained exception behavior; `Feature` for current Microsoft-backed runtime preservation, unsupported-path behavior, and boundary guardrails on touched shared seams
|
||||||
|
- **Affected validation lanes**: `fast-feedback`, `confidence`
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: The business risk is semantic drift inside shared code, not browser interaction. Unit tests prove classification and neutral-contract rules; feature tests prove current Microsoft-backed flows remain intact and unsupported shared-boundary cases fail explicitly.
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: Minimal. Reuse existing `ProviderConnection` and tenant factories plus current provider unit tests. Do not introduce a new default provider world helper.
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: No. The boundary catalog stays config-backed and test fixtures remain opt-in.
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none
|
||||||
|
- **Surface-class relief / special coverage rule**: `N/A`
|
||||||
|
- **Closing validation and reviewer handoff**: Reviewers should verify that `ProviderIdentityResolution` no longer shapes Graph request options, that shared operation metadata no longer treats `microsoft` as silent default truth, that the remaining Microsoft-specific identity fields are documented as exceptions, and that current Microsoft-backed starts still work through the hardened seams.
|
||||||
|
- **Budget / baseline / trend follow-up**: none expected
|
||||||
|
- **Review-stop questions**: Did any platform-core seam retain Graph request shaping? Did `ProviderOperationRegistry` still expose provider binding as primary platform truth? Did the slice widen into schema/UI neutrality or operation-type renaming? Did any new test helper make provider context implicit by default?
|
||||||
|
- **Escalation path**: `document-in-feature`
|
||||||
|
- **Active feature PR close-out entry**: `Guardrail`
|
||||||
|
- **Why no dedicated follow-up spec is needed**: The remaining identity-schema and UI neutrality work already has a named next candidate. This feature contains only the first bounded hardening pass.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/237-provider-boundary-hardening/
|
||||||
|
├── spec.md
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── provider-boundary-hardening.logical.openapi.yaml
|
||||||
|
└── tasks.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── Models/
|
||||||
|
│ │ └── ProviderConnection.php
|
||||||
|
│ ├── Services/
|
||||||
|
│ │ ├── Graph/
|
||||||
|
│ │ │ ├── GraphClientInterface.php
|
||||||
|
│ │ │ └── MicrosoftGraphClient.php
|
||||||
|
│ │ └── Providers/
|
||||||
|
│ │ ├── MicrosoftGraphOptionsResolver.php
|
||||||
|
│ │ ├── PlatformProviderIdentityResolver.php
|
||||||
|
│ │ ├── ProviderConnectionResolution.php
|
||||||
|
│ │ ├── ProviderConnectionResolver.php
|
||||||
|
│ │ ├── ProviderGateway.php
|
||||||
|
│ │ ├── ProviderIdentityResolution.php
|
||||||
|
│ │ ├── ProviderIdentityResolver.php
|
||||||
|
│ │ ├── ProviderOperationRegistry.php
|
||||||
|
│ │ └── ProviderOperationStartGate.php
|
||||||
|
│ └── Support/
|
||||||
|
│ └── Providers/
|
||||||
|
│ └── Boundary/
|
||||||
|
├── config/
|
||||||
|
│ └── provider_boundaries.php
|
||||||
|
└── tests/
|
||||||
|
├── Feature/
|
||||||
|
│ ├── Providers/
|
||||||
|
│ └── Guards/
|
||||||
|
└── Unit/
|
||||||
|
└── Providers/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Keep the entire slice inside the existing Laravel runtime in `apps/platform`. The only new top-level code shape is a small `Support/Providers/Boundary` namespace plus a config-backed seam catalog. Runtime changes stay inside the existing provider services and the shared provider operation registry path.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
No constitutional violation is planned. One bounded complexity addition is tracked because the feature introduces a new source of truth for seam ownership.
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| BLOAT-001 bounded boundary catalog | Multiple real shared seams now need one explicit, testable ownership source of truth so provider leakage stops depending on reviewer memory alone | Comments, prose-only notes, or local assertions would not be machine-checkable and would let each new seam drift independently |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: Shared provider-backed platform code can still silently become more Microsoft-shaped, which raises the cost and risk of future governance work even when current operator behavior still appears to work.
|
||||||
|
- **Existing structure is insufficient because**: generic class names and partial provider abstractions do not stop Graph request shaping, provider binding defaults, and Microsoft-specific semantics from leaking into shared resolution or orchestration paths.
|
||||||
|
- **Narrowest correct implementation**: add one config-backed seam catalog, extract Graph option shaping out of shared identity resolution, and separate provider binding from shared operation metadata at the existing registry/start-gate seam.
|
||||||
|
- **Ownership cost created**: maintain the seam catalog, preserve a small set of boundary guard tests, and keep the one documented Microsoft-first exception path explicit until the follow-up identity-neutrality work lands.
|
||||||
|
- **Alternative intentionally rejected**: a broad multi-provider framework or connector platform. It would import speculative runtime machinery before there is a second real provider case.
|
||||||
|
- **Release truth**: current-release truth with deliberate anti-drift preparation for the next provider-boundary follow-through specs
|
||||||
|
|
||||||
|
## Phase 0 Research Summary
|
||||||
|
|
||||||
|
- The first boundary hardening slice should use a small config-backed seam catalog, not a generic provider-plugin framework.
|
||||||
|
- `ProviderIdentityResolution::graphOptions()` is a concrete provider-leak hotspot because a shared resolution object currently shapes Microsoft Graph request options directly.
|
||||||
|
- `ProviderOperationRegistry` is a second concrete hotspot because shared operation definitions currently expose `microsoft` as if it were platform-default truth.
|
||||||
|
- Existing `ProviderGateway`, `MicrosoftGraphOptionsResolver`, and Intune-specific services are acceptable provider-owned seams for current Microsoft behavior.
|
||||||
|
- `entra_tenant_id`, platform app identity config, and callback/redirect details should remain explicit current-release exceptions here and be cleaned up in the follow-up identity-neutrality slice.
|
||||||
|
- Focused unit plus feature guard tests are sufficient; browser or heavy-governance coverage would add cost without proving unique behavior.
|
||||||
|
|
||||||
|
## Phase 1 Design Summary
|
||||||
|
|
||||||
|
- `research.md` records the boundary decisions that keep the slice narrow and explicit.
|
||||||
|
- `data-model.md` defines the seam ownership catalog, operation definition vs provider binding split, and boundary guard result shape.
|
||||||
|
- `contracts/provider-boundary-hardening.logical.openapi.yaml` defines the logical internal contract for listing seam ownership and evaluating boundary changes.
|
||||||
|
- `quickstart.md` records the narrow validation order and the intended code areas.
|
||||||
|
- `tasks.md` will sequence the work from seam-catalog foundation through shared identity and registry hardening to final guard coverage.
|
||||||
|
|
||||||
|
## Phase 1 — Agent Context Update
|
||||||
|
|
||||||
|
Run after artifact generation:
|
||||||
|
|
||||||
|
- `.specify/scripts/bash/update-agent-context.sh copilot`
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Phase A — Add the seam ownership catalog
|
||||||
|
|
||||||
|
**Goal**: Make the authoritative first-slice seams explicitly classifiable and testable.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| A.1 | `apps/platform/config/provider_boundaries.php` | Add the bounded seam catalog for `provider.gateway_runtime`, `provider.identity_resolution`, `provider.connection_resolution`, `provider.operation_registry`, and `provider.operation_start_gate`, classifying each as `provider_owned` or `platform_core` and recording retained-provider-semantic notes as exception metadata where needed. |
|
||||||
|
| A.2 | `apps/platform/app/Support/Providers/Boundary/ProviderBoundaryOwner.php`, `ProviderBoundarySeam.php`, and `ProviderBoundaryCatalog.php` | Model seam ownership, allowed exceptions, and deterministic lookup for tests and runtime guard checks. |
|
||||||
|
| A.3 | `apps/platform/tests/Unit/Providers/ProviderBoundaryClassificationTest.php` | Prove the catalog contains the intended first-slice seams and only the allowed ownership classifications. |
|
||||||
|
|
||||||
|
### Phase B — Move Graph request shaping behind provider-owned seams
|
||||||
|
|
||||||
|
**Goal**: Stop shared identity resolution from emitting Microsoft Graph-shaped runtime options directly.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| B.1 | `apps/platform/app/Services/Providers/ProviderIdentityResolution.php` | Remove Graph request-option shaping from the shared resolution object and expose only the neutral runtime data the provider-owned seam needs. |
|
||||||
|
| B.2 | `apps/platform/app/Services/Providers/ProviderGateway.php` and `MicrosoftGraphOptionsResolver.php` | Own Graph option assembly inside provider-owned seams and reuse the shared resolution data without reintroducing platform-core Graph leakage. |
|
||||||
|
| B.3 | `apps/platform/app/Services/Providers/ProviderIdentityResolver.php`, `PlatformProviderIdentityResolver.php`, and `ProviderConnectionResolver.php` | Keep current Microsoft-first identity semantics working while marking the remaining target-scope and platform-app details as explicit current-release exceptions. |
|
||||||
|
|
||||||
|
### Phase C — Split shared operation metadata from provider binding
|
||||||
|
|
||||||
|
**Goal**: Keep shared orchestration metadata platform-core while making provider binding explicit and bounded.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| C.1 | `apps/platform/app/Services/Providers/ProviderOperationRegistry.php` | Separate platform-core operation definition fields from provider-binding fields so the shared definition does not treat `microsoft` as silent default truth. |
|
||||||
|
| C.2 | `apps/platform/app/Services/Providers/ProviderOperationStartGate.php` | Consume the explicit provider binding, preserve current Microsoft-backed start behavior, and return explicit unsupported behavior when a touched shared seam has no provider-owned binding. |
|
||||||
|
| C.3 | `apps/platform/app/Support/Providers/ProviderReasonCodes.php` and adjacent translation helpers if needed | Add one narrow provider-boundary reason code only if explicit unsupported shared-boundary behavior needs stable runtime semantics. |
|
||||||
|
|
||||||
|
### Phase D — Add guardrails and preserve runtime behavior
|
||||||
|
|
||||||
|
**Goal**: Keep the boundary enforceable without widening the slice.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| D.1 | `apps/platform/tests/Unit/Providers/ProviderBoundaryGuardrailTest.php` | Prove platform-core seam rules, allowed exceptions, and registry split behavior are deterministic. |
|
||||||
|
| D.2 | `apps/platform/tests/Feature/Providers/ProviderBoundaryHardeningTest.php` | Prove a current Microsoft-backed workflow still succeeds through the hardened seams. |
|
||||||
|
| D.3 | `apps/platform/tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php` | Prove the touched shared seam fails explicitly rather than inheriting Microsoft default behavior when binding or ownership is absent. |
|
||||||
|
| D.4 | `specs/237-provider-boundary-hardening/quickstart.md` and `tasks.md` | Keep the validation order, exception boundary, and no-second-provider-runtime guardrail explicit. |
|
||||||
|
|
||||||
|
## Risks and Mitigations
|
||||||
|
|
||||||
|
- **Identity scope creep**: Boundary hardening could drift into full provider identity neutrality. Mitigation: keep `entra_tenant_id` and platform app identity as explicit current-release exceptions and defer schema/UI neutrality to the next spec.
|
||||||
|
- **Operation-type scope creep**: Registry cleanup could become operation-type canonicalization work. Mitigation: keep operation type values unchanged and limit this slice to ownership separation, not naming reform.
|
||||||
|
- **Guardrail overreach**: A broad filesystem scan could flag legitimate provider-owned Microsoft services. Mitigation: make the seam catalog the allowlist source of truth and keep the guard coverage focused on touched shared seams.
|
||||||
|
- **Runtime regression**: Moving Graph option shaping can break current Microsoft-backed flows. Mitigation: preserve and extend focused provider unit and feature coverage around the hardened gateway and registry paths.
|
||||||
|
|
||||||
|
## Post-Design Re-check
|
||||||
|
|
||||||
|
The feature remains constitution-compliant, Filament v5 and Livewire v4 compliant, and narrow. It introduces no new persistence, no new operator-facing page, no new provider runtime, and no operation-type renaming. The plan, research, data model, quickstart, contract, and later tasks align on one explicit seam catalog, one provider-owned Graph shaping boundary, one shared registry hardening step, and one bounded Microsoft-first exception path.
|
||||||
84
specs/237-provider-boundary-hardening/quickstart.md
Normal file
84
specs/237-provider-boundary-hardening/quickstart.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# Quickstart: Provider Boundary Hardening
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement the first provider-boundary hardening slice without introducing a second-provider runtime, schema churn, or new operator-facing UI.
|
||||||
|
|
||||||
|
## Implementation Sequence
|
||||||
|
|
||||||
|
1. Done: added the config-backed seam ownership catalog and the small boundary helper layer.
|
||||||
|
2. Done: removed Graph request-option shaping from `ProviderIdentityResolution` and kept it inside provider-owned gateway/options resolver seams.
|
||||||
|
3. Done: split shared operation definitions from provider bindings in `ProviderOperationRegistry` and made `ProviderOperationStartGate` consume explicit bindings.
|
||||||
|
4. Done: kept remaining Microsoft-first identity details documented as explicit current-release exceptions instead of widening into schema and UI neutrality.
|
||||||
|
5. Done: added focused unit and feature coverage proving current Microsoft behavior still works and unsupported shared-boundary paths fail explicitly.
|
||||||
|
|
||||||
|
## Suggested Code Areas
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/app/Support/Providers/Boundary/
|
||||||
|
apps/platform/config/provider_boundaries.php
|
||||||
|
apps/platform/app/Services/Providers/
|
||||||
|
apps/platform/app/Services/Graph/
|
||||||
|
apps/platform/app/Models/ProviderConnection.php
|
||||||
|
apps/platform/tests/Unit/Providers/
|
||||||
|
apps/platform/tests/Feature/Providers/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authoritative Seam Inventory
|
||||||
|
|
||||||
|
- `provider.gateway_runtime`
|
||||||
|
- `provider.identity_resolution`
|
||||||
|
- `provider.connection_resolution`
|
||||||
|
- `provider.operation_registry`
|
||||||
|
- `provider.operation_start_gate`
|
||||||
|
|
||||||
|
## Verification Commands
|
||||||
|
|
||||||
|
Run the narrowest proving lane first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the bounded runtime and unsupported-path proof:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the feature-guard proof that blocks provider leakage in platform-core seams:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
If PHP files were added or changed, finish with formatting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
- Confirm `ProviderIdentityResolution` no longer emits Graph-shaped request options directly.
|
||||||
|
- Confirm `ProviderGateway` and `MicrosoftGraphOptionsResolver` are the provider-owned seams that now assemble Graph request options.
|
||||||
|
- Confirm `ProviderOperationRegistry` keeps platform-core operation definition separate from provider binding.
|
||||||
|
- Confirm unsupported touched seams fail explicitly instead of inheriting Microsoft default behavior.
|
||||||
|
- Confirm `entra_tenant_id` and platform app identity remain explicit current-release exceptions, not silent platform-core truth.
|
||||||
|
- Confirm no second-provider runtime, schema rewrite, or UI surface slipped into the slice.
|
||||||
|
|
||||||
|
## Guardrail Close-Out
|
||||||
|
|
||||||
|
- Validation completed before final handoff:
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderGatewayTest.php tests/Unit/Providers/ProviderIdentityResolverTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- Guardrails checked:
|
||||||
|
- No new Graph contract path.
|
||||||
|
- No new provider runtime or marketplace abstraction.
|
||||||
|
- `ProviderIdentityResolution` no longer exposes `graphOptions()` or `client_request_id`.
|
||||||
|
- `ProviderOperationRegistry` keeps platform-core definitions separate from explicit provider bindings.
|
||||||
|
- `ProviderOperationStartGate` blocks unsupported provider bindings with `provider_binding_unsupported` instead of falling back to Microsoft.
|
||||||
|
- No undocumented provider-shaped exception on touched platform-core seams.
|
||||||
|
- Close-out decision: `document-in-feature`. The remaining Microsoft-first identity and target-scope details are bounded in `provider_boundaries.php`, covered by guardrails, and intentionally left for the next provider identity/target-scope neutrality spec.
|
||||||
|
- Bounded follow-up: deeper provider identity and target-scope neutrality remains a separate next spec; no schema rewrite, UI vocabulary rewrite, or second-provider runtime is included here.
|
||||||
42
specs/237-provider-boundary-hardening/research.md
Normal file
42
specs/237-provider-boundary-hardening/research.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Research: Provider Boundary Hardening
|
||||||
|
|
||||||
|
## Decision 1: Use a small config-backed seam catalog instead of a provider framework
|
||||||
|
|
||||||
|
- **Decision**: Model first-slice provider-boundary ownership in one repository config catalog plus a small boundary helper layer, not as a speculative multi-provider framework.
|
||||||
|
- **Rationale**: The current release needs explicit ownership and guardrails across multiple real seams more than it needs connector plugins, provider registries, or generic runtime extension points. A config-backed catalog is reviewable, deterministic, and easy to enforce in tests.
|
||||||
|
- **Boundary model note**: The catalog keeps seam ownership binary as `provider_owned` or `platform_core`. Any retained Microsoft-first behavior is recorded as seam metadata with an explicit follow-up action, not as a third ownership state.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Prose-only documentation and comments: rejected because reviewers cannot enforce it mechanically and the same drift can reappear on the next seam.
|
||||||
|
- Full provider-plugin architecture: rejected because there is still only one shipped provider runtime.
|
||||||
|
|
||||||
|
## Decision 2: Keep Graph request shaping inside provider-owned seams
|
||||||
|
|
||||||
|
- **Decision**: Remove Graph request-option shaping from `ProviderIdentityResolution` and keep it inside provider-owned seams such as `ProviderGateway` and `MicrosoftGraphOptionsResolver`.
|
||||||
|
- **Rationale**: A shared identity-resolution object currently knows Microsoft Graph request-option keys and request-id generation details. That is provider-owned behavior and should not live on a platform-core result type.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Leave `graphOptions()` on `ProviderIdentityResolution`: rejected because it preserves Graph semantics in a shared runtime type.
|
||||||
|
- Introduce a broad provider request-context framework: rejected because the narrower extraction into existing provider-owned seams is sufficient.
|
||||||
|
|
||||||
|
## Decision 3: Split shared operation definition from provider binding
|
||||||
|
|
||||||
|
- **Decision**: Keep platform-core operation metadata separate from provider binding metadata in `ProviderOperationRegistry` and the `ProviderOperationStartGate` path.
|
||||||
|
- **Rationale**: Operation type, module, label, and capability are shared orchestration truth. The fact that the current runtime binds those operations to `microsoft` is provider-owned current-release behavior and should be explicit rather than silent default truth.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Keep a single registry array with `provider => microsoft` on every entry: rejected because it makes the current first provider look like a permanent platform default.
|
||||||
|
- Fold this work into operation-type canonicalization: rejected because this spec is about ownership boundaries, not renaming operation codes.
|
||||||
|
|
||||||
|
## Decision 4: Treat target-scope and platform app identity details as bounded current-release exceptions
|
||||||
|
|
||||||
|
- **Decision**: Keep `entra_tenant_id`, platform app credential config, and callback-specific details as explicit current-release exceptions in this slice instead of widening into schema and UI neutrality.
|
||||||
|
- **Rationale**: These are real hotspots, but the next candidate `Provider Identity & Target Scope Neutrality` exists specifically to clean up the deeper persistence and operator-vocabulary consequences. This slice should harden the boundary before it rewrites identity truth.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Rename storage and UI semantics now: rejected because it would widen the slice into a second spec.
|
||||||
|
- Ignore the hotspot entirely: rejected because the plan needs one documented exception boundary rather than pretending the issue is solved.
|
||||||
|
|
||||||
|
## Decision 5: Enforce the boundary with focused unit and feature guardrails
|
||||||
|
|
||||||
|
- **Decision**: Prove the hardening with narrow unit and feature tests that exercise seam classification, provider-binding behavior, unsupported-path behavior, and Microsoft runtime preservation.
|
||||||
|
- **Rationale**: The risk is architectural drift inside shared services, not browser behavior. Focused code-level tests are the narrowest proof that the boundary is explicit and enforceable.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Browser or UI smoke coverage: rejected because the slice adds no new operator-facing surface.
|
||||||
|
- Manual review only: rejected because the feature exists specifically to remove dependence on reviewer memory.
|
||||||
235
specs/237-provider-boundary-hardening/spec.md
Normal file
235
specs/237-provider-boundary-hardening/spec.md
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
# Feature Specification: Provider Boundary Hardening
|
||||||
|
|
||||||
|
**Feature Branch**: `237-provider-boundary-hardening`
|
||||||
|
**Created**: 2026-04-24
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Provider Boundary Hardening"
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: TenantPilot already has generic-looking provider seams, but shared contracts and orchestration paths can still absorb Microsoft Graph or Entra semantics as if they were platform-core truth.
|
||||||
|
- **Today's failure**: Contributors can extend provider-backed workflows by deepening Microsoft-shaped fields, defaults, and fallback behavior inside shared platform seams, which quietly turns future portability into cross-cutting rewrite work.
|
||||||
|
- **User-visible improvement**: Current Microsoft-backed workflows stay intact while the platform core becomes more predictable, reviewable, and resistant to accidental provider leakage in future work.
|
||||||
|
- **Smallest enterprise-capable version**: Classify the highest-risk shared seams as provider-owned or platform-core, harden the shared contracts around neutral concepts, and add focused review and test guardrails that block new platform-core Graph leakage.
|
||||||
|
- **Explicit non-goals**: No second-provider runtime, no AWS or GCP adapters, no broad provider marketplace or registry redesign, no speculative generic connector framework, and no repo-wide copy cleanup beyond seams that are genuinely platform-core.
|
||||||
|
- **Permanent complexity imported**: One explicit provider-boundary classification model for in-scope seams, one stricter neutral-contract discipline for platform-core paths, one bounded exception model for current-release Microsoft truth, and focused regression coverage for guardrails.
|
||||||
|
- **Why now**: The roadmap places this immediately after Canonical Control Catalog Foundation in the near-term Governance Platform anti-drift sequence, and Spec 236 is already created.
|
||||||
|
- **Why not local**: Local fixes inside one adapter or one page would leave other shared seams free to keep importing Microsoft semantics, so the product would continue drifting even while individual hotspots look cleaner.
|
||||||
|
- **Approval class**: Core Enterprise
|
||||||
|
- **Red flags triggered**: New abstraction risk and future-proofing risk. Defense: the first slice stays tightly scoped to already hot shared seams, preserves Microsoft-first product truth, and avoids speculative multi-provider runtime work.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
|
||||||
|
- **Decision**: approve
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: workspace
|
||||||
|
- **Primary Routes**:
|
||||||
|
- No new standalone route is required in the hardening slice.
|
||||||
|
- Existing provider-backed admin, tenant-context, and monitoring flows remain on their current surfaces and consume the hardened seams behind the scenes.
|
||||||
|
- **Data Ownership**:
|
||||||
|
- No new tenant-owned or workspace-owned business entity is introduced.
|
||||||
|
- The first slice hardens shared contracts, boundary classification, and provider-owned metadata handling around existing provider and governance workflows.
|
||||||
|
- **RBAC**:
|
||||||
|
- No new top-level capability is introduced.
|
||||||
|
- Existing authorization on provider-backed workflows continues to gate downstream behavior; this spec must not relax workspace or tenant isolation.
|
||||||
|
|
||||||
|
For canonical-view specs, the spec MUST define:
|
||||||
|
|
||||||
|
- Not applicable in this slice because no new canonical-view surface or cross-tenant filter behavior is introduced.
|
||||||
|
|
||||||
|
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||||
|
|
||||||
|
- **Cross-cutting feature?**: yes
|
||||||
|
- **Interaction class(es)**: provider-backed orchestration, shared contracts, monitoring-adjacent execution seams, governed-subject resolution boundaries
|
||||||
|
- **Systems touched**: provider gateways, provider client adapters, shared orchestration or registry seams, and guardrails that review new provider-backed code paths
|
||||||
|
- **Existing pattern(s) to extend**: current provider gateway and provider client adapter seam, existing provider dispatch and operation orchestration boundaries, current provider-boundary constitution rules
|
||||||
|
- **Shared contract / presenter / builder / renderer to reuse**: existing provider gateway and provider-dispatch contracts stay in place; the feature hardens which side of the boundary owns provider-specific semantics rather than introducing a parallel path
|
||||||
|
- **Why the existing shared path is sufficient or insufficient**: the shared seams are sufficient as extension points, but they are insufficiently explicit today about whether touched behavior is provider-owned or platform-core, so contributors can still deepen Microsoft coupling by default
|
||||||
|
- **Allowed deviation and why**: bounded current-release Microsoft exceptions are allowed only where the seam is explicitly classified as provider-owned or where the spec records a narrow provider-specific necessity
|
||||||
|
- **Consistency impact**: in-scope shared contracts must use one neutral platform vocabulary, and provider-specific descriptors, identifiers, payload assumptions, and fallback behavior must remain on the provider-owned side of the seam
|
||||||
|
- **Review focus**: reviewers should block any new shared-path change that exposes Graph- or Entra-shaped semantics as platform-core truth without an explicit boundary classification or approved exception
|
||||||
|
|
||||||
|
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Boundary classification**: binary ownership with documented current-release exception metadata
|
||||||
|
- **Seams affected**: provider gateways, client adapters, orchestration entry points, registry or resolution seams that decide provider-backed behavior, and shared vocabulary used by those contracts
|
||||||
|
- **Authoritative first-slice seam inventory**:
|
||||||
|
- `provider.gateway_runtime` -> provider-owned -> `ProviderGateway.php`, `MicrosoftGraphOptionsResolver.php`
|
||||||
|
- `provider.identity_resolution` -> platform-core with documented exception metadata -> `ProviderIdentityResolution.php`, `ProviderIdentityResolver.php`, `PlatformProviderIdentityResolver.php`
|
||||||
|
- `provider.connection_resolution` -> platform-core with documented exception metadata -> `ProviderConnectionResolver.php`, `ProviderConnectionResolution.php`
|
||||||
|
- `provider.operation_registry` -> platform-core -> `ProviderOperationRegistry.php`
|
||||||
|
- `provider.operation_start_gate` -> platform-core -> `ProviderOperationStartGate.php`
|
||||||
|
- **Neutral platform terms preserved or introduced**: provider, provider connection, target scope, operation type, governed subject, provider-owned metadata, platform-core contract, unsupported combination
|
||||||
|
- **Provider-specific semantics retained and why**: Microsoft Graph payload shapes, Entra and tenant identifiers, Microsoft workload descriptors, and any provider-specific transport or error details remain provider-owned because current product truth is still Microsoft-first
|
||||||
|
- **Why this does not deepen provider coupling accidentally**: the first slice makes the boundary explicit, keeps seam ownership binary, and requires any retained provider-specific semantics to live either behind provider-owned contracts or as named exception metadata on a platform-core seam instead of becoming a third ownership class
|
||||||
|
- **Follow-up path**: Provider Identity & Target Scope Neutrality and Platform Vocabulary Boundary Enforcement for Governed Subject Keys build on this boundary hardening rather than redefining it
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||||
|
|
||||||
|
N/A - no operator-facing surface change is required in the hardening slice.
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: yes
|
||||||
|
- **New persisted entity/table/artifact?**: no
|
||||||
|
- **New abstraction?**: yes
|
||||||
|
- **New enum/state/reason family?**: yes - seam ownership and boundary-check result values are new; runtime `ProviderReasonCodes` expansion remains conditional on whether existing unsupported-path handling is insufficient
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: no
|
||||||
|
- **Current operator problem**: The product can still become more provider-shaped with each new shared-path change, which raises the long-term cost of governance features and increases the risk that platform-visible behavior is really Microsoft behavior with generic names.
|
||||||
|
- **Existing structure is insufficient because**: existing generic class names and seams do not by themselves prevent provider-specific identifiers, fallback semantics, or vocabulary from becoming platform-core truth.
|
||||||
|
- **Narrowest correct implementation**: classify only the highest-risk shared seams, keep current Microsoft behavior where it already belongs, harden neutral contracts at those seams, and add guardrails instead of broad speculative architecture.
|
||||||
|
- **Ownership cost**: maintainers must preserve the seam classifications, uphold the neutral-contract rules in review, and keep focused tests aligned as provider-backed workflows evolve.
|
||||||
|
- **Alternative intentionally rejected**: a full multi-provider abstraction rewrite was rejected because it imports speculative scope; pure local cleanup was rejected because it leaves the repo free to continue drifting at every other shared seam.
|
||||||
|
- **Release truth**: current-release truth with deliberate anti-drift preparation for later provider-boundary follow-through
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment.
|
||||||
|
|
||||||
|
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
|
||||||
|
|
||||||
|
Canonical replacement is preferred over preservation.
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Unit, Feature
|
||||||
|
- **Validation lane(s)**: fast-feedback, confidence
|
||||||
|
- **Why this classification and these lanes are sufficient**: the first slice is a contract-boundary hardening effort. Unit coverage proves seam classification and neutral-contract rules, while focused feature coverage proves that current Microsoft-backed workflows still behave correctly through the hardened seams.
|
||||||
|
- **New or expanded test families**: targeted provider-boundary guardrail tests only
|
||||||
|
- **Fixture / helper cost impact**: minimal; prefer existing provider fixtures and narrow seam-specific helpers over new broad provider test scaffolding
|
||||||
|
- **Heavy-family visibility / justification**: none; no browser or heavy-governance lane is justified for this slice
|
||||||
|
- **Special surface test profile**: N/A
|
||||||
|
- **Standard-native relief or required special coverage**: ordinary feature coverage only
|
||||||
|
- **Reviewer handoff**: reviewers should confirm that the tests prove boundary ownership and unsupported-path handling, not just class existence, and that no new heavy provider harness becomes the default path for ordinary seam checks
|
||||||
|
- **Budget / baseline / trend impact**: none expected
|
||||||
|
- **Escalation needed**: none
|
||||||
|
- **Active feature PR close-out entry**: Guardrail
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderBoundaryClassificationTest.php tests/Unit/Providers/ProviderBoundaryGuardrailTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Providers/ProviderBoundaryHardeningTest.php tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Classify Shared Seams Before Extending Them (Priority: P1)
|
||||||
|
|
||||||
|
As a maintainer extending a provider-backed workflow, I want to know whether a seam is provider-owned or platform-core before I change it so I do not accidentally encode Microsoft semantics into shared platform truth.
|
||||||
|
|
||||||
|
**Why this priority**: This is the smallest valuable slice. If the seam classification is explicit, later changes can stay bounded even before every follow-on cleanup is delivered.
|
||||||
|
|
||||||
|
**Independent Test**: Inspect a targeted shared seam, classify it, and prove that a contributor can determine where provider-specific semantics are allowed without guessing from class names alone.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a shared provider-backed seam is in scope for the hardening slice, **When** a maintainer reviews it, **Then** the seam is explicitly classified as provider-owned or platform-core.
|
||||||
|
2. **Given** a seam is classified as platform-core, **When** a change introduces Microsoft-specific identifiers or payload assumptions there, **Then** the change is rejected by the defined guardrails rather than silently accepted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Keep Microsoft Truth Bounded Without Breaking Current Behavior (Priority: P1)
|
||||||
|
|
||||||
|
As a product maintainer, I want current Microsoft-backed workflows to continue working while Microsoft-specific semantics move or remain behind provider-owned boundaries so the anti-drift work does not regress the current release.
|
||||||
|
|
||||||
|
**Why this priority**: The hardening only matters if it is shippable without destabilizing the product's actual Microsoft-first workflows.
|
||||||
|
|
||||||
|
**Independent Test**: Exercise at least one hardened Microsoft-backed path and confirm the user-visible behavior stays the same while the shared seam no longer owns provider-specific truth.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a provider-owned adapter is responsible for Microsoft-specific request or response handling, **When** the shared platform seam calls it, **Then** the Microsoft behavior still completes correctly without moving Graph semantics into the platform-core contract.
|
||||||
|
2. **Given** a provider-specific concept is not supported at a shared seam, **When** the platform-core path encounters it, **Then** the result is an explicit unsupported outcome instead of an inherited Microsoft default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Catch New Provider Leakage in Review and CI (Priority: P2)
|
||||||
|
|
||||||
|
As a reviewer, I want narrow tests and guardrails that catch new platform-core provider leakage so the boundary does not depend on tribal knowledge.
|
||||||
|
|
||||||
|
**Why this priority**: Without enforceable guardrails, the spec would describe a preferred architecture but not actually keep the repo from drifting back.
|
||||||
|
|
||||||
|
**Independent Test**: Introduce a representative provider-specific field or fallback into a platform-core seam and verify that the new guardrail coverage fails.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a new platform-core contract change introduces provider-specific vocabulary without an approved exception, **When** the targeted tests or review guardrails run, **Then** the change fails visibly.
|
||||||
|
2. **Given** a provider-owned seam intentionally keeps Microsoft-specific metadata, **When** the same guardrails run, **Then** the bounded provider-owned usage is allowed and documented instead of being treated as a false violation.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- One in-scope seam contains both neutral platform fields and provider-owned metadata and must separate them without inventing duplicate contracts.
|
||||||
|
- A historical shared path still uses Graph- or Entra-shaped field names even though the seam is now classified as platform-core.
|
||||||
|
- Only one provider exists in production today, so unsupported-path behavior must stay explicit without pretending a second provider runtime already exists.
|
||||||
|
- A shared orchestration path currently falls back to Microsoft-first behavior when no explicit provider handler is present.
|
||||||
|
- A provider-owned surface intentionally uses Microsoft vocabulary because the operator is configuring a Microsoft-specific capability, while the shared contract under it must remain neutral.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** This feature does not add Microsoft Graph calls, destructive mutations, or new queued work. It hardens where existing provider-backed runtime behavior may encode provider-specific semantics into platform-core truth. If implementation touches existing writes or operations, that slice must keep current tenant isolation and existing observability intact.
|
||||||
|
|
||||||
|
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature intentionally introduces one explicit boundary-classification model and one stricter neutral-contract discipline because generic class names alone have not prevented provider leakage. The first slice stays derived and review-oriented, avoids new persistence, and avoids speculative multi-provider abstractions.
|
||||||
|
|
||||||
|
**Constitution alignment (XCUT-001):** This feature is cross-cutting across provider gateways, orchestration, and shared contracts. It extends existing shared seams rather than creating parallel provider infrastructure, and reviewer focus stays on preventing new local Microsoft-shaped paths from appearing inside platform-core code.
|
||||||
|
|
||||||
|
**Constitution alignment (PROV-001):** Every in-scope seam must be classified as provider-owned or platform-core. Provider-specific semantics remain on provider-owned paths unless a narrow current-release exception is documented. Shared platform contracts preserve neutral platform vocabulary and explicit unsupported-path behavior.
|
||||||
|
|
||||||
|
**Constitution alignment (TEST-GOV-001):** Coverage stays in narrow unit and feature lanes. Tests must prove boundary ownership and unsupported-path semantics on the touched seams without introducing a broad provider integration harness.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-UX):** Not applicable in this slice because it does not create or redesign an `OperationRun`.
|
||||||
|
|
||||||
|
**Constitution alignment (RBAC-UX):** No authorization boundary changes are introduced. Existing authorization continues to protect provider-backed flows on their current surfaces.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable.
|
||||||
|
|
||||||
|
**Constitution alignment (BADGE-001):** Not applicable in this slice because no new status badge family is introduced.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-FIL-001):** Not applicable in this slice because no Filament surface change is required.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-NAMING-001):** If implementation touches any operator-visible provider descriptor as part of a hardened seam, Microsoft-specific wording must remain contextual to the provider-owned surface and must not redefine the shared platform contract.
|
||||||
|
|
||||||
|
**Constitution alignment (DECIDE-001):** Not applicable in this slice because no new or materially changed operator-facing decision surface is required.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** Not applicable in this slice because no operator-facing surface is added or materially refactored.
|
||||||
|
|
||||||
|
**Constitution alignment (ACTSURF-001 - action hierarchy):** Not applicable.
|
||||||
|
|
||||||
|
**Constitution alignment (OPSURF-001):** Not applicable in this slice because there is no new operator-facing page contract.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** The feature adds one interpretation layer for boundary ownership because direct reuse of current class names and runtime paths is insufficient to keep provider-specific truth out of platform-core seams. Tests must focus on boundary consequences rather than thin wrappers.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-237-001 Boundary ownership classification**: The system MUST define one explicit boundary-ownership classification for every high-risk shared seam touched by this feature, identifying it as provider-owned or platform-core. Retained current-release exceptions MUST be recorded as seam metadata, not as a third ownership class.
|
||||||
|
- **FR-237-002 Neutral platform-core contracts**: Platform-core seams in scope MUST use neutral platform vocabulary and MUST NOT expose Microsoft- or Graph-specific identifiers, payload semantics, or fallback assumptions as their primary contract.
|
||||||
|
- **FR-237-003 Provider-owned metadata containment**: Microsoft-specific request, response, transport, verification, and error semantics MUST remain behind provider-owned adapters or services.
|
||||||
|
- **FR-237-004 Hardened shared seam coverage**: The first slice MUST harden at least the current provider gateway seam and at least one shared orchestration or registry seam where provider leakage currently risks becoming platform-core truth.
|
||||||
|
- **FR-237-005 Explicit unsupported behavior**: If a platform-core seam encounters a provider-specific behavior with no explicit provider-owned handler, the outcome MUST be explicit unsupported behavior rather than inherited Microsoft default behavior.
|
||||||
|
- **FR-237-006 Current-release behavior preservation**: The hardened seams MUST preserve current Microsoft-backed product behavior for already supported flows.
|
||||||
|
- **FR-237-007 Exception discipline**: Any retained provider-specific semantics at a shared seam MUST be documented as a narrow current-release exception and MUST NOT become the default meaning of the platform-core contract.
|
||||||
|
- **FR-237-008 Review and test guardrails**: The repo MUST provide focused review or test guardrails that fail when new platform-core provider leakage is introduced on an in-scope seam.
|
||||||
|
- **FR-237-009 Provider-owned allowance**: The same guardrails MUST allow provider-specific metadata and logic on seams explicitly classified as provider-owned.
|
||||||
|
- **FR-237-010 No speculative multi-provider runtime**: The first slice MUST NOT introduce second-provider execution paths, provider marketplace abstractions, or generic connector runtime machinery.
|
||||||
|
- **FR-237-011 Shared vocabulary continuity**: Follow-on provider-boundary work for identity, target scope, or governed-subject vocabulary MUST be able to build on the same boundary classifications instead of redefining them locally.
|
||||||
|
- **FR-237-012 Explicit shared orchestration binding**: Shared orchestration paths touched by this feature MUST require an explicit provider binding instead of inferring `microsoft` as an implicit default when the platform-core path resolves an operation.
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Boundary Ownership Classification**: The explicit determination that a touched seam is either provider-owned or platform-core, with any narrow current-release exception recorded as metadata on that seam.
|
||||||
|
- **Platform-Core Contract**: A shared contract that expresses provider-neutral platform concepts and delegates provider-specific behavior to provider-owned seams.
|
||||||
|
- **Provider-Owned Adapter**: The seam that translates Microsoft-specific identifiers, payloads, verification details, and errors into or out of platform-core contracts without redefining platform truth.
|
||||||
|
- **Boundary Guardrail Result**: The focused review or test outcome that confirms whether a changed seam respects the declared ownership and neutral-contract rules.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- Current-release product truth remains Microsoft-first, but Microsoft semantics must stay bounded where the seam is platform-core.
|
||||||
|
- Existing provider gateway and orchestration hotspots are sufficient to prove the first slice without introducing a new provider runtime.
|
||||||
|
- Identity, target-scope, and governed-subject follow-through specs will consume this boundary decision instead of reopening it from scratch.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-237-001**: 100% of high-risk shared seams touched in the first slice are explicitly classified as provider-owned or platform-core before implementation is considered complete.
|
||||||
|
- **SC-237-002**: 100% of platform-core seams hardened by this spec avoid Microsoft- or Graph-specific primary contract fields or fallback semantics.
|
||||||
|
- **SC-237-003**: At least one already supported Microsoft-backed workflow continues to pass through the hardened seams without user-visible regression.
|
||||||
|
- **SC-237-004**: In all covered unsupported-path scenarios for the hardened seams, the outcome is explicit unsupported behavior rather than inherited Microsoft default behavior.
|
||||||
226
specs/237-provider-boundary-hardening/tasks.md
Normal file
226
specs/237-provider-boundary-hardening/tasks.md
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
description: "Task list for Provider Boundary Hardening"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tasks: Provider Boundary Hardening
|
||||||
|
|
||||||
|
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/237-provider-boundary-hardening/`
|
||||||
|
**Prerequisites**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/237-provider-boundary-hardening/plan.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/237-provider-boundary-hardening/spec.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/237-provider-boundary-hardening/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/237-provider-boundary-hardening/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/237-provider-boundary-hardening/contracts/`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/237-provider-boundary-hardening/quickstart.md`
|
||||||
|
|
||||||
|
**Tests**: REQUIRED (Pest) for runtime behavior changes; keep proof in the narrow `Unit` and `Feature` lanes named in the plan
|
||||||
|
**Operations**: No new `OperationRun` type or new monitoring surface is introduced; preserve current `ProviderOperationStartGate` behavior while making provider binding explicit
|
||||||
|
**RBAC**: No authorization plane changes are planned; preserve existing tenant and workspace enforcement on touched provider-backed flows
|
||||||
|
**Provider Boundary**: Every touched seam must be classified as `provider_owned` or `platform_core`, and any retained Microsoft-shaped behavior must remain explicitly bounded as documented seam metadata rather than a third ownership class
|
||||||
|
|
||||||
|
**Organization**: Tasks are grouped by user story so the seam-classification slice, runtime hardening slice, and guardrail slice can be delivered and validated incrementally.
|
||||||
|
|
||||||
|
## Test Governance Checklist
|
||||||
|
|
||||||
|
- [X] Lane assignment stays `Unit` plus `Feature` and remains the narrowest sufficient proof for the changed behavior.
|
||||||
|
- [X] New or changed tests stay in the existing provider test families; no browser or heavy-governance lane is added.
|
||||||
|
- [X] Shared helpers, factories, fixtures, and provider context defaults must stay cheap by default; do not introduce a default multi-provider harness.
|
||||||
|
- [X] Planned validation commands must cover the boundary catalog, runtime preservation, unsupported-path handling, and registry split without widening scope.
|
||||||
|
- [X] Surface test profile remains `N/A` because this slice adds no new operator-facing screen.
|
||||||
|
- [X] Any remaining Microsoft-first hotspot must resolve as `document-in-feature` or `follow-up-spec`, not as silent platform-core truth.
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Context)
|
||||||
|
|
||||||
|
**Purpose**: Confirm the baseline hotspots, current proof lanes, and existing guard patterns before implementation starts.
|
||||||
|
|
||||||
|
- [X] T001 Review the current hotspot seams in `apps/platform/app/Services/Providers/ProviderGateway.php`, `apps/platform/app/Services/Providers/ProviderIdentityResolution.php`, `apps/platform/app/Services/Providers/ProviderOperationRegistry.php`, and `apps/platform/app/Services/Providers/ProviderOperationStartGate.php`
|
||||||
|
- [X] T002 Run the existing provider baseline tests for `apps/platform/tests/Unit/Providers/ProviderGatewayTest.php` and `apps/platform/tests/Unit/Providers/ProviderIdentityResolverTest.php`
|
||||||
|
- [X] T003 [P] Review the existing boundary-guard patterns in `apps/platform/tests/Feature/Guards/NoLegacyTenantGraphOptionsTest.php` and `apps/platform/tests/Feature/Guards/NoLegacyTenantProviderFallbackTest.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Add the shared boundary catalog primitives that every user story depends on.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: No user story work should start until these tasks are complete.
|
||||||
|
|
||||||
|
- [X] T004 Create the seam ownership catalog scaffold in `apps/platform/config/provider_boundaries.php`
|
||||||
|
- [X] T005 [P] Add the ownership enum in `apps/platform/app/Support/Providers/Boundary/ProviderBoundaryOwner.php`
|
||||||
|
- [X] T006 [P] Add the seam descriptor value object in `apps/platform/app/Support/Providers/Boundary/ProviderBoundarySeam.php`
|
||||||
|
- [X] T007 Implement deterministic catalog lookup in `apps/platform/app/Support/Providers/Boundary/ProviderBoundaryCatalog.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Shared boundary primitives exist; user story work can now build on one explicit source of truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Classify Shared Seams Before Extending Them (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Make the first high-risk shared provider seams explicitly classifiable so contributors can tell where Microsoft-specific semantics are allowed.
|
||||||
|
|
||||||
|
**Independent Test**: Run `tests/Unit/Providers/ProviderBoundaryClassificationTest.php` and verify the authoritative seam keys `provider.gateway_runtime`, `provider.identity_resolution`, `provider.connection_resolution`, `provider.operation_registry`, and `provider.operation_start_gate` resolve to the intended owner classification and exception metadata.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
- [X] T008 [P] [US1] Add seam classification coverage in `apps/platform/tests/Unit/Providers/ProviderBoundaryClassificationTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [X] T009 [US1] Populate the authoritative first-slice seam entries `provider.gateway_runtime`, `provider.identity_resolution`, `provider.connection_resolution`, `provider.operation_registry`, and `provider.operation_start_gate` in `apps/platform/config/provider_boundaries.php`
|
||||||
|
- [X] T010 [US1] Implement seam hydration and ownership assertions in `apps/platform/app/Support/Providers/Boundary/ProviderBoundaryCatalog.php`
|
||||||
|
- [X] T011 [US1] Encode neutral terms, retained provider semantics, and follow-up actions in `apps/platform/config/provider_boundaries.php` and `apps/platform/app/Support/Providers/Boundary/ProviderBoundarySeam.php`
|
||||||
|
- [X] T012 [US1] Align the seam catalog shape with `specs/237-provider-boundary-hardening/contracts/provider-boundary-hardening.logical.openapi.yaml` through `apps/platform/config/provider_boundaries.php` and `apps/platform/app/Support/Providers/Boundary/ProviderBoundaryCatalog.php`
|
||||||
|
- [X] T013 [US1] Run the story proof lane for `apps/platform/tests/Unit/Providers/ProviderBoundaryClassificationTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: The seam catalog is explicit, deterministic, and independently testable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Keep Microsoft Truth Bounded Without Breaking Current Behavior (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Move Graph-shaped runtime behavior behind provider-owned seams while preserving current Microsoft-backed flows and making unsupported paths explicit.
|
||||||
|
|
||||||
|
**Independent Test**: Run the runtime regression and unsupported-path tests to confirm current Microsoft behavior still succeeds and platform-core seams no longer own Graph option shaping.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
- [X] T014 [P] [US2] Extend provider runtime regression coverage in `apps/platform/tests/Unit/Providers/ProviderGatewayTest.php` and `apps/platform/tests/Unit/Providers/ProviderIdentityResolverTest.php`
|
||||||
|
- [X] T015 [P] [US2] Add Microsoft runtime preservation coverage in `apps/platform/tests/Feature/Providers/ProviderBoundaryHardeningTest.php`
|
||||||
|
- [X] T016 [P] [US2] Add explicit unsupported-path coverage in `apps/platform/tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [X] T017 [US2] Remove Graph request-option shaping from `apps/platform/app/Services/Providers/ProviderIdentityResolution.php`
|
||||||
|
- [X] T018 [US2] Move Graph option assembly into `apps/platform/app/Services/Providers/ProviderGateway.php` and `apps/platform/app/Services/Providers/MicrosoftGraphOptionsResolver.php`
|
||||||
|
- [X] T019 [US2] Preserve bounded Microsoft-first identity exceptions in `apps/platform/app/Services/Providers/ProviderIdentityResolver.php`, `apps/platform/app/Services/Providers/PlatformProviderIdentityResolver.php`, `apps/platform/app/Services/Providers/ProviderConnectionResolver.php`, and `apps/platform/app/Services/Providers/ProviderConnectionResolution.php`
|
||||||
|
- [X] T020 [US2] Make the current-release exception metadata explicit in `apps/platform/config/provider_boundaries.php` and `apps/platform/app/Support/Providers/Boundary/ProviderBoundarySeam.php`
|
||||||
|
- [X] T021 [US2] Run the story proof lane for `apps/platform/tests/Unit/Providers/ProviderGatewayTest.php`, `apps/platform/tests/Unit/Providers/ProviderIdentityResolverTest.php`, `apps/platform/tests/Feature/Providers/ProviderBoundaryHardeningTest.php`, and `apps/platform/tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Current Microsoft-backed runtime behavior still works, and the shared identity path no longer emits Graph-shaped platform-core truth.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Catch New Provider Leakage in Review and CI (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Split shared operation definition from provider binding and add focused guardrails that fail when provider-specific semantics leak back into platform-core seams.
|
||||||
|
|
||||||
|
**Independent Test**: Run the guardrail and start-gate tests to confirm platform-core/provider-owned boundaries are enforced and unsupported bindings fail explicitly.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
- [X] T022 [P] [US3] Add a CI boundary leak guard in `apps/platform/tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`
|
||||||
|
- [X] T023 [P] [US3] Add boundary leak and exception guard coverage in `apps/platform/tests/Unit/Providers/ProviderBoundaryGuardrailTest.php`
|
||||||
|
- [X] T024 [P] [US3] Extend provider binding and unsupported-start coverage in `apps/platform/tests/Unit/Providers/ProviderOperationStartGateTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [X] T025 [US3] Split platform-core operation definition from provider binding in `apps/platform/app/Services/Providers/ProviderOperationRegistry.php`
|
||||||
|
- [X] T026 [US3] Consume explicit bindings and unsupported outcomes in `apps/platform/app/Services/Providers/ProviderOperationStartGate.php`
|
||||||
|
- [X] T027 [US3] Re-use existing unsupported-path reason handling, or add one narrow boundary violation and unsupported-binding reason in `apps/platform/app/Support/Providers/ProviderReasonCodes.php` only if the explicit shared-boundary outcome cannot be expressed without it
|
||||||
|
- [X] T028 [US3] Record binding status, handler notes, and exception notes for first-slice operations in `apps/platform/app/Services/Providers/ProviderOperationRegistry.php` and `apps/platform/config/provider_boundaries.php`
|
||||||
|
- [X] T029 [US3] Align the operation definition and binding split with `specs/237-provider-boundary-hardening/contracts/provider-boundary-hardening.logical.openapi.yaml` through `apps/platform/app/Services/Providers/ProviderOperationRegistry.php` and `apps/platform/app/Services/Providers/ProviderOperationStartGate.php`
|
||||||
|
- [X] T030 [US3] Run the story proof lane for `apps/platform/tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`, `apps/platform/tests/Unit/Providers/ProviderBoundaryGuardrailTest.php`, `apps/platform/tests/Unit/Providers/ProviderOperationStartGateTest.php`, and `apps/platform/tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Provider binding is explicit, guardrails fail on new leakage, and shared orchestration no longer silently defaults to Microsoft-first behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Finalize validation, formatting, and guardrail close-out across the full slice.
|
||||||
|
|
||||||
|
- [X] T031 [P] Refresh the implementation notes, validation commands, and bounded follow-up status in `specs/237-provider-boundary-hardening/quickstart.md`
|
||||||
|
- [X] T032 Run formatting for `apps/platform/app/Support/Providers/Boundary/`, `apps/platform/app/Services/Providers/`, `apps/platform/tests/Unit/Providers/`, and `apps/platform/tests/Feature/Guards/` with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- [X] T033 Run the final narrow validation lane for `apps/platform/tests/Unit/Providers/ProviderBoundaryClassificationTest.php`, `apps/platform/tests/Unit/Providers/ProviderBoundaryGuardrailTest.php`, `apps/platform/tests/Feature/Providers/ProviderBoundaryHardeningTest.php`, `apps/platform/tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php`, and `apps/platform/tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php`
|
||||||
|
- [X] T034 Record the guardrail close-out, `document-in-feature` decision, and deferred identity-neutrality follow-up in `specs/237-provider-boundary-hardening/quickstart.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### User Story Dependency Graph
|
||||||
|
|
||||||
|
```text
|
||||||
|
Phase 1 (Setup)
|
||||||
|
↓
|
||||||
|
Phase 2 (Foundation: seam catalog primitives)
|
||||||
|
↓
|
||||||
|
US1 (Seam classification)
|
||||||
|
↓
|
||||||
|
US2 (Runtime hardening and explicit exceptions)
|
||||||
|
↓
|
||||||
|
US3 (Registry split and guardrails)
|
||||||
|
↓
|
||||||
|
Phase 6 (Polish)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Setup (Phase 1)**: No dependencies; starts immediately.
|
||||||
|
- **Foundational (Phase 2)**: Depends on Setup; blocks all user story work.
|
||||||
|
- **User Story 1 (Phase 3)**: Depends on the boundary catalog primitives from Phase 2.
|
||||||
|
- **User Story 2 (Phase 4)**: Depends on US1 because runtime hardening consumes the explicit seam ownership catalog.
|
||||||
|
- **User Story 3 (Phase 5)**: Depends on US1 and US2 because registry guardrails must reflect the classified seams and the hardened runtime path.
|
||||||
|
- **Polish (Phase 6)**: Depends on all desired user stories being complete.
|
||||||
|
|
||||||
|
### Parallel Opportunities
|
||||||
|
|
||||||
|
- Phase 2 tasks `T005` and `T006` can run in parallel because they touch different support classes.
|
||||||
|
- US1 tasks `T008` and `T010` can run in parallel after the catalog scaffold exists because the test file and catalog service are separate files.
|
||||||
|
- US2 tasks `T014`, `T015`, and `T016` can run in parallel because they cover different test files.
|
||||||
|
- US3 tasks `T022`, `T023`, and `T024` can run in parallel because they extend separate guard and unit test files.
|
||||||
|
- Phase 6 tasks `T031` and `T032` can run in parallel because they touch documentation and formatting independently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Example: User Story 1
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Task: "Add seam classification coverage in apps/platform/tests/Unit/Providers/ProviderBoundaryClassificationTest.php"
|
||||||
|
Task: "Implement seam hydration and ownership assertions in apps/platform/app/Support/Providers/Boundary/ProviderBoundaryCatalog.php"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Example: User Story 2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Task: "Extend provider runtime regression coverage in apps/platform/tests/Unit/Providers/ProviderGatewayTest.php and apps/platform/tests/Unit/Providers/ProviderIdentityResolverTest.php"
|
||||||
|
Task: "Add Microsoft runtime preservation coverage in apps/platform/tests/Feature/Providers/ProviderBoundaryHardeningTest.php"
|
||||||
|
Task: "Add explicit unsupported-path coverage in apps/platform/tests/Feature/Providers/UnsupportedProviderBoundaryPathTest.php"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Example: User Story 3
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Task: "Add boundary leak and exception guard coverage in apps/platform/tests/Unit/Providers/ProviderBoundaryGuardrailTest.php"
|
||||||
|
Task: "Extend provider binding and unsupported-start coverage in apps/platform/tests/Unit/Providers/ProviderOperationStartGateTest.php"
|
||||||
|
Task: "Add a CI boundary leak guard in apps/platform/tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (User Story 1 Only)
|
||||||
|
|
||||||
|
1. Complete Phase 1: Setup.
|
||||||
|
2. Complete Phase 2: Foundational seam catalog primitives.
|
||||||
|
3. Complete Phase 3: User Story 1.
|
||||||
|
4. Stop and validate `apps/platform/tests/Unit/Providers/ProviderBoundaryClassificationTest.php`.
|
||||||
|
5. Review whether the first-slice seam catalog is explicit enough before widening into runtime cleanup.
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Setup + Foundation establish one explicit boundary source of truth.
|
||||||
|
2. US1 delivers seam classification and explicit exception metadata documentation.
|
||||||
|
3. US2 hardens the shared runtime path while preserving current Microsoft-backed behavior.
|
||||||
|
4. US3 makes provider binding explicit and adds CI-proof guardrails.
|
||||||
|
5. Polish closes formatting, validation, and the documented follow-up boundary.
|
||||||
|
|
||||||
|
### Suggested MVP Scope
|
||||||
|
|
||||||
|
The narrowest shippable increment is Phase 1, Phase 2, and Phase 3 only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `[P]` tasks touch different files and can be worked independently.
|
||||||
|
- `[US1]`, `[US2]`, and `[US3]` map directly to the user stories in `spec.md`.
|
||||||
|
- Keep provider-specific semantics bounded to provider-owned seams; do not introduce a second-provider runtime while completing these tasks.
|
||||||
|
- Use the Sail-prefixed validation commands from `specs/237-provider-boundary-hardening/quickstart.md` when executing the proof lanes.
|
||||||
Loading…
Reference in New Issue
Block a user