# Implementation Plan: Central Tenant Operability Policy **Branch**: `148-central-tenant-operability-policy` | **Date**: 2026-03-17 | **Spec**: [specs/148-central-tenant-operability-policy/spec.md](./spec.md) **Input**: Feature specification from `/specs/148-central-tenant-operability-policy/spec.md` **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. ## Summary Consolidate existing tenant lifecycle and selector semantics into one actor-aware, lane-aware tenant operability authority that returns reusable structured decisions instead of scattered booleans. The implementation will evolve the existing `TenantOperabilityService` and related tenant-support types into a central policy boundary consumed by selector membership, remembered-context revalidation, tenant action catalogs, tenant-bound route checks, onboarding workflow surfaces, and canonical workspace record viewers. This is a support-layer hardening feature, not a persistence or UI-redesign feature. The plan therefore focuses on extending the current support seams already present in `app/Services/Tenants`, `app/Support/Tenants`, `app/Support/OperateHub`, and `app/Support/Workspaces`, then migrating the highest-risk consumers first: choose-tenant and context persistence, `OperateHubShell`, `TenantActionPolicySurface`, `TenantResource`, onboarding workflow routes, and `TenantlessOperationRunViewer`. ## Technical Context **Language/Version**: PHP 8.4.15 **Primary Dependencies**: Laravel 12, Filament 5, Livewire 4, Pest 4, existing support-layer helpers such as `UiEnforcement`, `CapabilityResolver`, `WorkspaceContext`, `OperateHubShell`, `TenantOperabilityService`, and `TenantActionPolicySurface` **Storage**: PostgreSQL plus existing session-backed workspace and remembered-tenant context; no schema change planned for the first implementation slice **Testing**: Pest 4 feature, unit, and Filament or Livewire-focused tests run through Laravel Sail **Target Platform**: Laravel Sail web application serving the Filament admin panel and workspace-canonical `/admin` routes **Project Type**: Laravel monolith web application **Performance Goals**: Keep operability evaluation synchronous, deterministic, and DB or session-backed only; add no render-time external calls; avoid material query-count regressions on the chooser, tenant detail, and canonical operations viewer **Constraints**: Preserve Spec 143 lifecycle semantics, Spec 144 canonical-viewer authority, Spec 145 action taxonomy, Spec 146 badge centralization, and Spec 147 selector semantics; keep Livewire v4 and Filament v5 compliance; leave panel-provider registration unchanged in `bootstrap/providers.php`; keep `TenantResource` globally searchable only through its existing View or Edit pages; no new asset pipeline or `filament:assets` change; destructive actions remain `->action(...)->requiresConfirmation()`; 404 versus 403 semantics must stay explicit and server-side **Scale/Scope**: One central support-layer policy boundary plus adoption across the workspace shell, choose-tenant flow, remembered-context handling, tenant management resource, onboarding workflow surfaces, canonical operation viewer, and focused regression suites under `tests/Feature` and `tests/Unit` ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* **Pre-Phase 0 Gate: PASS** - Inventory-first: PASS. This feature does not change inventory, snapshot, or backup ownership and only hardens policy evaluation over existing records. - Read/write separation: PASS. No new remote writes, queued workflows, or lifecycle mutation mechanics are introduced. Existing archive and restore actions remain confirmed and audited where already implemented. - Graph contract path: PASS. No Microsoft Graph calls are added or changed. - Deterministic capabilities: PASS. Capability checks remain routed through the existing canonical capability registry and resolver services. - RBAC-UX planes: PASS. This feature remains in the admin `/admin` plane. Cross-plane behavior is unchanged. Non-members stay 404, in-scope capability denial stays 403. - Workspace isolation: PASS. Workspace remains the primary operating boundary and remembered tenant context remains workspace-scoped convenience state only. - Tenant isolation: PASS. Tenant-linked routes and canonical viewers must still enforce workspace membership plus tenant entitlement where applicable. - Destructive confirmation: PASS. No new destructive actions are added. Existing `Archive` and `Restore` remain `->action(...)->requiresConfirmation()` and capability-gated. - Global search safety: PASS WITH REQUIRED ADOPTION. `TenantResource` is already globally searchable and has View and Edit pages, satisfying Filament v5 global search requirements. The implementation must ensure selector eligibility is not incorrectly reused as the only discoverability rule. - Run observability and Ops-UX: PASS. No new `OperationRun` creation, lifecycle transition, notification, or summary-count behavior is introduced. - Badge semantics (BADGE-001): PASS. Lifecycle and status presentation stay centralized through existing badge and lifecycle presentation helpers. - UI naming (UI-NAMING-001): PASS. Operability explanations will reuse existing domain terms such as `Archive`, `Restore`, `Resume onboarding`, `selected tenant`, `route tenant`, and `run tenant`. - Filament Action Surface Contract: PASS. In-scope Filament surfaces already exist; this feature changes their decision authority, not their required action inventory. Any touched lifecycle mutation remains confirmation-required and audit-aware. - Filament UX-001: PASS. No layout redesign is planned. Existing pages keep their current layout contracts while consuming central policy outcomes. - Asset strategy: PASS. No new Filament or front-end assets are planned, so no deployment change to `php artisan filament:assets` is required. **Post-Phase 1 Re-check: PASS** - The design extends existing support-layer abstractions instead of adding a parallel policy subsystem. - No database migration, queue job, Graph contract change, panel change, or asset build change is required for the first implementation slice. - Livewire v4 and Filament v5 compliance remain intact, and provider registration continues to live in `bootstrap/providers.php`. - `TenantResource` remains globally searchable through existing View or Edit pages; the design only changes search scoping inputs, not the resource’s page contract. ## Project Structure ### Documentation (this feature) ```text specs/148-central-tenant-operability-policy/ ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── tenant-operability-policy.openapi.yaml └── tasks.md ``` ### Source Code (repository root) ```text app/ ├── Filament/ │ ├── Pages/ │ │ ├── Operations/ │ │ ├── Workspaces/ │ │ └── ChooseTenant.php │ └── Resources/ │ └── TenantResource.php ├── Http/ │ └── Controllers/ ├── Models/ │ ├── OperationRun.php │ ├── Tenant.php │ ├── TenantOnboardingSession.php │ ├── User.php │ └── UserTenantPreference.php ├── Policies/ ├── Services/ │ ├── Auth/ │ ├── Onboarding/ │ └── Tenants/ └── Support/ ├── Filament/ ├── Middleware/ ├── OperateHub/ ├── Rbac/ ├── Tenants/ └── Workspaces/ routes/ └── web.php resources/ └── views/ └── filament/ tests/ ├── Feature/ │ ├── Operations/ │ ├── Onboarding/ │ ├── Rbac/ │ └── Filament/ └── Unit/ └── Tenants/ ``` **Structure Decision**: Use the existing Laravel monolith and strengthen the support layer rather than creating a new policy module outside the current architecture. The implementation seam stays inside `app/Services/Tenants` and `app/Support/Tenants`, with adoption routed through `WorkspaceContext`, `OperateHubShell`, existing Filament resources and pages, and existing auth or UI enforcement helpers. ## Phase 0 Research Summary - The repo already contains a partial centralization layer, but it is not sufficient for Spec 148. `TenantOperabilityService` currently returns lifecycle-derived booleans and only accepts a `Tenant`; it does not accept actor, lane, linked-record context, or capability context. - `TenantActionPolicySurface` is already centralizing labels and action descriptors, which means the right direction is to feed it richer operability outcomes rather than replace it with another action-assembly system. - `OperateHubShell` currently mixes route-authoritative tenant resolution, Filament tenant state, and remembered tenant fallback. Its entitlement logic uses `TenantPageCategory`, but outside tenant-bound routes it still relies on `canSelectAsContext`, which is too narrow for administrative discoverability and canonical linked-record handling. - `ChooseTenant` and `SelectTenantController` duplicate selector-eligibility checks and persistence flow. These should become thin consumers of the shared operability policy and shared remembered-context validation path. - `TenantResource::getGlobalSearchEloquentQuery()` currently reuses `applySelectableScope()`, which means selector eligibility is already influencing discoverability. Spec 148 needs a clear separation between selector lane eligibility and administrative discoverability. - Route safety foundations are already present. `Tenant::resolveRouteBinding()` loads soft-deleted tenants for external IDs, `TenantPageCategory` already classifies canonical and onboarding routes, and `TenantlessOperationRunViewer` already treats tenant mismatch as informational. The feature should formalize and centralize the same semantics instead of re-deriving them locally. - Laravel documentation confirms the intended 404 versus 403 split should use authorization responses such as `Response::denyAsNotFound()` for non-members or non-entitled access and normal Gate authorization for in-scope capability denial. - Filament documentation confirms destructive actions must remain `->action(...)->requiresConfirmation()` and that URL-only actions cannot be confirmation-modal actions. Existing lifecycle actions already align with this and the implementation should preserve that contract. ## Phase 1 Design ### Implementation Approach 1. Evolve the existing operability seam into the single authority. - Keep `TenantOperabilityService` as the implementation anchor, but expand it from tenant-only booleans into a central policy surface that accepts explicit context. - Preserve backward compatibility temporarily by providing adapter methods for existing consumers while migrating them to structured outcomes. 2. Add explicit lane-aware and reason-aware domain objects. - Introduce or extend support-layer types for interaction lane, structured input context, structured outcome, and reason codes. - Keep these types in `app/Support/Tenants` so they remain close to existing lifecycle and page-category enums. - `TenantOperabilityContext` carries normalized record, lane, route, and workflow inputs supplied by the consumer, while `TenantOperabilityService` resolves live membership, entitlement, capability, and archived-state facts through existing authoritative helpers so callers do not precompute authorization truth. 3. Separate operability from authorization while composing them intentionally. - Operability decides whether the requested behavior is meaningful for lifecycle and lane. - Authorization remains server-side truth for membership, entitlement, and capability. - The structured outcome must distinguish at least wrong lane, lifecycle mismatch, non-selector eligibility, missing capability, and non-entitled access. 4. Centralize consumer adoption around the highest-risk surfaces first. - Selector construction and remembered-context validation. - `OperateHubShell` active-tenant resolution. - `TenantActionPolicySurface` and `TenantResource` action visibility. - Onboarding workflow resumability checks. - Canonical viewer tenant-linked affordance and tenant-bound route support checks. 5. Split selector eligibility from discoverability. - Replace the current assumption that active-selector membership and administrative visibility are the same semantic decision. - Create distinct policy questions for standard selector eligibility, administrative discoverability, tenant-bound viewability, and canonical reference validity. 6. Preserve route-authority semantics from Specs 144 and 147. - Tenant-bound pages remain valid from route tenant plus entitlement. - Canonical workspace viewers remain valid from the workspace-owned record plus entitlement. - Selected tenant context remains a filter or convenience input only. 7. Keep the first slice schema-free and asset-free. - No new persistent table is required for structured outcomes or reason codes. - No new panel, provider registration, or Filament asset work is planned. ### Planned Workstreams - **Workstream A: Operability core model** Extend `TenantOperabilityService` and `TenantOperabilityDecision` into a central lane-aware policy boundary with structured inputs and reasoned outcomes. - **Workstream B: Selector and remembered-context adoption** Refactor `ChooseTenant`, `SelectTenantController`, and `WorkspaceContext` to use the new operability contract for standard selector membership and remembered-context validity. - **Workstream C: Shell and route adoption** Update `OperateHubShell`, route-adjacent middleware, and tenant-page classification consumers so administrative, tenant-bound, and canonical-record lanes request the right operability question instead of reusing selector booleans. - **Workstream D: Action-surface adoption** Refactor `TenantActionPolicySurface` and `TenantResource` lifecycle actions to consume action-specific operability outcomes and reason codes while preserving `UiEnforcement`, confirmation, and audit behavior. - **Workstream E: Discoverability and global-search hardening** Separate administrative discoverability from standard selector eligibility for `TenantResource`, choose-tenant, and any shared tenant-picking or search surfaces in scope. - **Workstream F: Regression hardening** Add focused Pest coverage for lifecycle state, lane differences, selector membership, remembered-context invalidation, canonical viewer safety, tenant-bound route safety, lifecycle-action eligibility, and capability-denied versus lifecycle-denied distinctions. ### Testing Strategy - Add unit tests for the central operability service covering each lifecycle across each lane and asserting structured reason codes for denied outcomes. - Add unit tests for selector eligibility, remembered-context validity, tenant-bound viewability, canonical linked-record viewability, archive eligibility, restore eligibility, and resume-onboarding eligibility. - Add focused feature tests for `ChooseTenant` and `SelectTenantController` to prove only active-lane eligible tenants can be selected. - Add focused feature tests for `WorkspaceContext` or route-level consumers to prove stale remembered context is cleared when the tenant becomes ineligible, missing, or out of workspace. - Add focused feature tests for `/admin/tenants/{tenant}` showing onboarding and archived tenants remain route-valid when authorized even though they are not selector-eligible. - Add focused feature tests for `/admin/operations/{run}` showing canonical viewer legitimacy does not depend on selected tenant context. - Add or update tests around `TenantActionPolicySurface` and `TenantResource` to prove `Archive`, `Restore`, and `Resume onboarding` remain lifecycle-safe and capability-aware. - Add focused tests proving onboarding-completion plus readiness or verification affordances resolve from the central operability layer with capability-aware and lifecycle-aware denial reasons. - Add focused search or resource-query tests proving tenant global search and administrative discoverability do not collapse back to selector-only semantics. - Run the minimum focused Pest suite through Sail; no full suite is required for planning artifacts. ## Complexity Tracking No constitution violations or exceptional complexity are planned at this stage.