Implements Spec 110 Ops‑UX Enforcement and applies the repo‑wide “enterprise” standard for operation start + dedup surfaces. Key points - Start surfaces: only ephemeral queued toast (no DB notifications for started/queued/running). - Dedup paths: canonical “already queued” toast. - Progress refresh: dispatch run-enqueued browser event so the global widget updates immediately. - Completion: exactly-once terminal DB notification on completion (per Ops‑UX contract). Tests & formatting - Full suite: 1738 passed, 8 skipped (8477 assertions). - Pint: `vendor/bin/sail bin pint --dirty --format agent` (pass). Notable change - Removed legacy `RunStatusChangedNotification` (replaced by the terminal-only completion notification policy). Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #134
22 KiB
TenantPilot Constitution
Core Principles
Inventory-first, Snapshots-second
- All modules MUST operate primarily on Inventory as “last observed” state.
- Inventory is the source of truth for what TenantPilot last observed; Microsoft Intune remains the external truth.
- Snapshots/Backups MUST be explicit actions (manual or scheduled) and MUST remain immutable.
Read/Write Separation by Default
- Analysis, reporting, and monitoring features MUST be read-only by default.
- Any write/change function (restore, remediation, promotion) MUST include preview/dry-run, explicit confirmation, audit logging, and tests.
- High-risk policy types default to
preview-onlyrestore unless explicitly enabled by a feature spec + tests.
Single Contract Path to Graph
- All Microsoft Graph calls MUST go through
GraphClientInterface. - Object types and endpoints MUST be modeled first in the contract registry (
config/graph_contracts.php). - Feature code MUST NOT hardcode “quick endpoints” or bypass contracts.
- Unknown/missing policy types MUST fail safe (preview-only / no Graph calls) rather than guessing endpoints.
Deterministic Capabilities
- Backup/restore/risk/support flags MUST be derived deterministically from config/contracts via a Capabilities Resolver.
- The resolver output MUST be programmatically testable (snapshot/golden tests) so config changes cannot silently break behavior.
Workspace Isolation is Non-negotiable
- Workspace membership is an isolation boundary. If the actor is not entitled to the workspace scope, the system MUST respond as deny-as-not-found (404).
- Workspace is the primary session context. Tenant-scoped routes/resources MUST require an established workspace context.
- Workspace context switching is separate from Filament Tenancy (Managed Tenant switching).
Tenant Isolation is Non-negotiable
- Every tenant-plane read/write MUST be tenant-scoped.
- Cross-tenant views (MSP/Platform) MUST be explicit, access-checked, and aggregation-based (no ID-based shortcuts).
- Tenantless canonical views (e.g., Monitoring/Operations) MUST enforce tenant entitlement before revealing records.
- Prefer least-privilege roles/scopes; surface warnings when higher privileges are selected.
- Tenant membership is an isolation boundary. If the actor is not entitled to the tenant scope, the system MUST respond as deny-as-not-found (404).
Scope & Ownership Clarification (SCOPE-001)
- The system MUST enforce a strict ownership model:
- Workspace-owned objects define standards, templates, and global configuration (e.g., Baseline Profiles, Notification Targets, Alert Routing Rules, Framework/Control catalogs).
- Tenant-owned objects represent observed state, evidence, and operational artifacts for a specific tenant (e.g., Inventory, Backups/Snapshots, OperationRuns for tenant operations, Drift/Findings, Exceptions/Risk Acceptance, EvidenceItems, StoredReports/Exports).
- Workspace-owned objects MUST NOT directly embed or persist tenant-owned records (no “copying tenant data into templates”).
- Tenant-owned objects MUST always be bound to an established workspace + tenant scope at authorization time.
Database convention:
- Tenant-owned tables MUST include workspace_id and tenant_id as NOT NULL.
- Workspace-owned tables MUST include workspace_id and MUST NOT include tenant_id.
- Exception: OperationRun MAY have tenant_id nullable to support canonical workspace-context monitoring views; however, revealing any tenant-bound runs still MUST enforce entitlement checks to the referenced tenant scope.
- Exception: AlertDelivery MAY have tenant_id nullable for workspace-scoped, non-tenant-operational artifacts (e.g.,
event_type=alerts.test). Tenant-bound delivery records still MUST enforce tenant entitlement checks, and tenantless delivery rows MUST NOT contain tenant-specific data.
RBAC & UI Enforcement Standards (RBAC-UX)
RBAC Context — Planes, Roles, and Auditability
- The platform MUST maintain two strictly separated authorization planes:
- Tenant/Admin plane (
/admin): authenticated Entra users (users). - Tenant-context routes (
/admin/t/{tenant}/...) are tenant-scoped. - Workspace-context canonical routes (
/admin/..., e.g. Monitoring/Operations) are tenantless by URL but MUST still enforce workspace + tenant entitlement before revealing tenant-owned records. - Platform plane (
/system): authenticated platform users (platform_users), authorization is platform-scoped.
- Tenant/Admin plane (
- Cross-plane access MUST be deny-as-not-found (404) (not 403) to avoid route enumeration.
- Tenant role semantics MUST remain least-privilege:
- Readonly: view-only; MUST NOT start operations and MUST NOT mutate data.
- Operator: MAY start allowed tenant operations; MUST NOT manage credentials, settings, members, or perform destructive actions.
- Manager: MAY manage tenant configuration and start operations; MUST NOT manage tenant memberships (Owner-only).
- Owner: MAY manage memberships and all tenant configuration; Owner-only “danger zone” actions MUST remain Owner-only.
- The system MUST prevent removing or demoting the last remaining Owner of a tenant.
- All access-control relevant changes MUST write
AuditLogentries with stable action IDs, and MUST be redacted (no secrets).
RBAC-UX-001 — Server-side is the source of truth
- UI visibility / disabled state is never a security boundary.
- Every mutating action (create/update/delete/restore/archive/force-delete), every operation start, and every credential/
config change MUST enforce authorization server-side via
Gate::authorize(...)or a Policy method. - Any missing server-side authorization is a P0 security bug.
RBAC-UX-002 — Deny-as-not-found for non-members
- Tenant and workspace membership (and plane membership) are isolation boundaries.
- If the current actor is not a member of the current workspace OR the current tenant (or otherwise not entitled to the workspace/tenant scope), the system MUST respond as 404 (deny-as-not-found) for tenant-scoped routes/actions/resources.
- This applies to Filament resources/pages under tenant routing (
/admin/t/{tenant}/...), Global Search results, and all action endpoints (Livewire calls included).
RBAC-UX-003 — Capability denial is 403 (after membership is established)
- Within an established workspace + tenant scope, missing permissions are authorization failures.
- If the actor is a workspace + tenant member, but lacks the required capability for an action, the server MUST fail with 403.
- The UI may render disabled actions, but the server MUST still enforce 403 on execution.
RBAC-UX-004 — Visible vs disabled UX rule
- For tenant members: actions SHOULD be visible but disabled when capability is missing.
- Disabled actions MUST provide helper text explaining the missing permission.
- For non-members: actions MUST behave as not found (404) and SHOULD NOT leak resource existence.
- Exception: highly sensitive controls (e.g., credential rotation) MAY be hidden even for members without permission.
RBAC-UX-005 — Destructive confirmation standard
- All destructive-like actions MUST require confirmation.
- Delete/force-delete/archive/restore/remove membership/role downgrade/credential rotation/break-glass enter/exit MUST use
->requiresConfirmation()and SHOULD include clear warning text. - Confirmation is UX only; authorization still MUST be server-side.
RBAC-UX-006 — Capability registry is canonical
- Capabilities MUST be centrally defined in a single canonical registry (constants/enum).
- Feature code MUST reference capabilities only via the registry (no raw string literals).
- Role → capability mapping MUST reference only registry entries.
- CI MUST fail if unknown/unregistered capabilities are used.
RBAC-UX-007 — Global search must be tenant-safe
- Global search MUST be context-safe (workspace-context vs tenant-context).
- Non-members MUST never learn about resources in other tenants (no results, no hints).
- If a result exists but is not accessible, it MUST be treated as not found (404 semantics).
- In workspace-context (no active tenant selected), Global Search MUST NOT return tenant-owned results.
- It MAY search workspace-owned objects only (e.g., Tenants list entries, Baseline Profiles, Alert Rules/Targets, workspace settings).
- If tenant-context is active, Global Search MUST be scoped to the current tenant only (existing rule remains).
RBAC-UX-008 — Regression guards are mandatory
- The repo MUST include RBAC regression tests asserting at least:
- Readonly cannot mutate or start operations.
- Operator can run allowed operations but cannot manage configuration.
- Manager/Owner behave according to the role matrix.
- The repo SHOULD include an automated “no ad-hoc authorization” guard that blocks new status/permission mappings sprinkled
across
app/Filament/**, pushing patterns into central helpers.
Operations / Run Observability Standard
- Every long-running or operationally relevant action MUST be observable, deduplicated, and auditable via Monitoring → Operations.
- An action MUST create/reuse a canonical
OperationRunand execute asynchronously when any of the following applies:- It can take > 2 seconds under normal conditions.
- It performs remote/external calls (e.g., Microsoft Graph).
- It is queued or scheduled.
- It is operationally relevant for troubleshooting/audit (“what ran, who started it, did it succeed, what failed?”).
- Actions that are DB-only and typically complete in < 2 seconds MAY skip
OperationRun. - OPS-EX-AUTH-001 — Auth Handshake Exception:
- OIDC/SAML login handshakes MAY perform synchronous outbound HTTP (e.g., token exchange) without an
OperationRun. - Rationale: interactive, session-critical, and not a tenant-operational “background job”.
- Guardrail: outbound HTTP for auth handshakes is allowed only on
/auth/*endpoints and MUST NOT occur on Monitoring/Operations pages.
- OIDC/SAML login handshakes MAY perform synchronous outbound HTTP (e.g., token exchange) without an
- If an action is security-relevant or affects operational behavior (e.g., “Ignore policy”), it MUST write an
AuditLogentry including actor, tenant, action, target, before/after, and timestamp. - The
OperationRunrecord is the canonical source of truth for Monitoring (status, timestamps, counts, failures), 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. - 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, confirm + “View run”.
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:
- Toast (intent only / queued-only)
- A toast MAY be shown only when the run is accepted/queued (intent feedback).
- The toast MUST use
OperationUxPresenter::queuedToast($operationType)->send(). - Feature code MUST NOT craft ad-hoc operation toasts.
- A dedicated dedupe message MUST use the presenter (e.g.,
alreadyQueuedToast(...)), notNotification::make().
- Progress (active awareness only)
- Live progress MUST exist only in:
- the global active-ops widget, and
- Monitoring → Operation Run Detail.
- These surfaces MUST show only active runs (
queued|running) and MUST never show terminal runs. - Determinate progress is allowed ONLY when
summary_counts.totalandsummary_counts.processedare valid numeric values. - Determinate progress MUST be clamped to 0–100. Otherwise render indeterminate + elapsed time.
- The widget MUST NOT show percentage text (optional
processed/totalis allowed).
- Terminal DB Notification (audit outcome only)
- Each run MUST emit exactly one persistent terminal DB notification when it becomes terminal.
- Delivery MUST be initiator-only (no tenant-wide fan-out).
- Completion notifications MUST be
OperationRunCompletedonly. - Feature code MUST NOT send custom completion DB notifications for operations (no
sendToDatabase()for completion/abort).
Canonical navigation:
- All “View run” links MUST use the canonical helper and MUST point to Monitoring → Operations → Run Detail.
OperationRun lifecycle is service-owned (OPS-UX-LC-001)
Any change to OperationRun.status or OperationRun.outcome MUST go through OperationRunService (canonical transition method).
This is the only allowed path because it enforces normalization, summary sanitization, idempotency, and terminal notification emission.
Forbidden outside OperationRunService:
$operationRun->update(['status' => ...])/$operationRun->update(['outcome' => ...])$operationRun->status = .../$operationRun->outcome = ...- Query-based updates that transition
status/outcome
Allowed outside the service:
- Updates to
context,message,reason_codethat do not changestatus/outcome.
Summary counts contract (OPS-UX-SUM-001)
operation_runs.summary_countsis the canonical metrics source for Ops-UX.- All keys MUST come from
OperationSummaryKeys::all()(single source of truth). - Values MUST be flat numeric-only; no nested objects/arrays; no free-text.
- Producers MUST NOT introduce new keys without:
- updating
OperationSummaryKeys::all(), - updating the spec canonical list,
- adding/adjusting tests.
- updating
Ops-UX regression guards are mandatory (OPS-UX-GUARD-001)
The repo MUST include automated guards (Pest) that fail CI if:
- any direct
OperationRunstatus/outcome transition occurs outsideOperationRunService, - jobs emit DB notifications for operation completion/abort (
OperationRunCompletedis the single terminal notification), - deprecated legacy operation notification classes are referenced again.
These guards MUST fail with actionable output (file + snippet).
Scheduled/system runs (OPS-UX-SYS-001)
- If a run has no initiator user, no terminal DB notification is emitted (initiator-only policy).
- Outcomes remain auditable via Monitoring → Operations / Run Detail.
- Any tenant-wide alerting MUST go through the Alerts system (not
OperationRunnotifications). - Active-run dedupe MUST be enforced at DB level (partial unique index/constraint for active states).
- Failures MUST be stored as stable reason codes + sanitized messages; never persist secrets/tokens/PII/raw payload dumps in failures or notifications.
- Graph calls are allowed only via explicit user interaction and only when delegated auth is present; never as a render side-effect (restore group mapping is intentionally DB-only).
- Monitoring → Operations is reserved for
OperationRun-tracked operations. - Scheduled/queued operations MUST use locks + idempotency (no duplicates).
- Graph throttling and transient failures MUST be handled with backoff + jitter (e.g., 429/503).
Filament UI — Action Surface Contract (NON-NEGOTIABLE)
For every new or modified Filament Resource / RelationManager / Page:
Required surfaces
- List/Table MUST define: Header Actions, Row Actions, Bulk Actions, and Empty-State CTA(s).
- Inspect affordance (List/Table): Every table MUST provide a record inspection affordance.
- Accepted forms: clickable rows via
recordUrl()(preferred), a dedicated row “View” action, or a primary linked column. - Rule: Do NOT render a lone “View” row action button. If View is the only row action, prefer clickable rows.
- Accepted forms: clickable rows via
- View/Detail MUST define Header Actions (Edit + “More” group when applicable).
- View/Detail MUST be sectioned (e.g., Infolist Sections / Cards); avoid long ungrouped field lists.
- Create/Edit MUST provide consistent Save/Cancel UX.
Grouping & safety
- Max 2 visible Row Actions (typically View/Edit). Everything else MUST be in an ActionGroup “More”.
- Bulk actions MUST be grouped via BulkActionGroup.
- RelationManagers MUST follow the same action surface rules (grouped row actions, bulk actions where applicable, inspection affordance).
- Destructive actions MUST NOT be primary and MUST require confirmation; typed confirmation MAY be required for large/bulk changes.
- Relevant mutations MUST write an audit log entry.
RBAC enforcement
- Non-member access MUST abort(404) and MUST NOT leak existence.
- Member without capability: UI visible but disabled with tooltip; server-side MUST abort(403).
- Central enforcement helpers (tenant/workspace UI enforcement) MUST be used for gating.
Spec / DoD gates
- Every spec MUST include a “UI Action Matrix”.
- A change is not “Done” unless the Action Surface Contract is met OR an explicit exemption exists with documented reason.
- CI MUST run an automated Action Surface Contract check (test suite and/or command) that fails when required surfaces are missing.
Filament UI — Layout & Information Architecture Standards (UX-001)
Goal: Demo-level, enterprise-grade admin UX. These rules are NON-NEGOTIABLE for new or modified Filament screens.
Page layout
- Create/Edit MUST default to a Main/Aside layout using a 3-column grid with
Main=columnSpan(2)andAside=columnSpan(1). - All fields MUST be inside Sections/Cards. No “naked” inputs at the root schema level.
- Main contains domain definition/content. Aside contains status/meta (status, version label, owner, scope selectors, timestamps).
- Related data (assignments, relations, evidence, runs, findings, etc.) MUST render as separate Sections below the main/aside grid (or as tabs/sub-navigation), never mixed as unstructured long lists.
View pages
- View/Detail MUST be a read-only experience using Infolists (or equivalent), not disabled edit forms.
- Status-like values MUST render as badges/chips using the centralized badge semantics (BADGE-001).
- Long text MUST render as readable prose (not textarea styling).
Empty states
- Empty lists/tables MUST show: a specific title, one-sentence explanation, and exactly ONE primary CTA in the empty state.
- When non-empty, the primary CTA MUST move to the table header (top-right) and MUST NOT be duplicated in the empty state.
Actions & flows
- Pages SHOULD expose at most 1 primary header action and 1 secondary action; all other actions MUST be grouped (ActionGroup / BulkActionGroup).
- Multi-step or high-risk flows MUST use a Wizard (e.g., capture/compare/restore with preview + confirmation).
- Destructive actions MUST remain non-primary and require confirmation (RBAC-UX-005).
Table work-surface defaults
- Tables SHOULD provide search (when the dataset can grow), a meaningful default sort, and filters for core dimensions (status/severity/type/tenant/time-range).
- Tables MUST render key statuses as badges/chips (BADGE-001) and avoid ad-hoc status mappings.
Enforcement
- New resources/pages SHOULD use shared layout builders (e.g.,
MainAsideForm,MainAsideInfolist,StandardTableDefaults) to keep screens consistent. - A change is not “Done” unless UX-001 is satisfied OR an explicit exemption exists with a documented rationale in the spec/PR.
Spec Scope Fields (SCOPE-002)
- Every feature spec MUST declare:
- Scope: workspace | tenant | canonical-view
- Primary Routes
- Data Ownership: workspace-owned vs tenant-owned tables/records impacted
- RBAC: membership requirements + capability requirements
- For canonical-view specs, the spec MUST define:
- Default filter behavior when tenant-context is active (e.g., prefilter to current tenant)
- Explicit entitlement checks that prevent cross-tenant leakage
Data Minimization & Safe Logging
- Inventory MUST store only metadata + whitelisted
meta_jsonb. - Payload-heavy content belongs in immutable snapshots/backup storage, not Inventory.
- Logs MUST not contain secrets/tokens; monitoring MUST rely on run records + error codes (not log parsing).
Badge Semantics Are Centralized (BADGE-001)
- Status-like badges (status/outcome/severity/risk/availability/boolean signals) MUST render via
BadgeCatalog/BadgeRenderer. - Filament resources/pages/widgets/views MUST NOT introduce ad-hoc status-like badge mappings (use a
BadgeDomaininstead). - Introducing or changing a status-like value MUST include updating the relevant badge mapper and adding/updating tests for the mapping.
- Tag/category chips (e.g., type/platform/environment) are not status-like and are not governed by BADGE-001.
Spec-First Workflow
- For any feature that changes runtime behavior, include or update
specs/<NNN>-<slug>/withspec.md,plan.md,tasks.md, andchecklists/requirements.md. - New work branches from
devusingfeat/<NNN>-<slug>(spec + code in the same PR).
Quality Gates
- Changes MUST be programmatically tested (Pest) and run via targeted
php artisan test .... - Run
./vendor/bin/sail bin pint --dirtybefore finalizing.
Governance
Scope & Compliance
- This constitution applies across the repo. Feature specs may add stricter constraints but not weaker ones.
- Restore semantics changes require: spec update, checklist update (if applicable), and tests proving safety.
Amendment Procedure
- Propose changes as a PR that updates
.specify/memory/constitution.md. - The PR MUST include a short rationale and list of impacted templates/specs.
- Amendments MUST update Last Amended date.
Versioning Policy (SemVer)
- PATCH: clarifications/typos/non-semantic refinements.
- MINOR: new principle/section or materially expanded guidance.
- MAJOR: removing/redefining principles in a backward-incompatible way.
Version: 1.10.0 | Ratified: 2026-01-03 | Last Amended: 2026-02-23