From 649a601dcaf11bbc86f3dfdf34b40e6a3819475a Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 13 May 2026 23:52:06 +0100 Subject: [PATCH] Hide resource privacy settings sidebar entry Keep the resource privacy policy route available for direct governance proof, but remove it from the normal Settings sidebar while it remains an informational read-only surface. Clarify the direct route copy and empty state so it does not appear as a broken settings page. --- docs/release-control/v6/internal/status.json | 2 +- .../subsystems/frontend-primitives.md | 10 +- .../internal/subsystems/security-privacy.md | 18 ++- .../components/Settings/DataHandlingPanel.tsx | 153 +++++++++++++----- .../__tests__/DataHandlingPanel.test.tsx | 80 +++++++++ .../__tests__/settingsArchitecture.test.ts | 28 +++- .../settingsNavigation.integration.test.tsx | 3 +- .../components/Settings/settingsHeaderMeta.ts | 4 +- .../components/Settings/settingsNavCatalog.ts | 3 +- 9 files changed, 236 insertions(+), 65 deletions(-) create mode 100644 frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index b8b8b3048..9476d35d8 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -3842,7 +3842,7 @@ "status": "partial", "completion": { "state": "bounded-residual", - "summary": "Policy-aware data governance now has a first-class governed floor: resource sensitivity, handling-boundary, and redaction posture are visible in Data Handling, non-local AI provider requests use the shared resource-policy sanitizer at the final model boundary, and privacy copy now reflects the runtime behavior. Broader enterprise DLP, provider-retention posture, and non-resource prompt-secret detection remain a named post-RC hardening track.", + "summary": "Policy-aware data governance now has a first-class governed floor: resource sensitivity, handling-boundary, and redaction posture remain available through the route-backed Resource Privacy/Data Handling surface without occupying the normal Settings sidebar as an empty read-only destination, non-local AI provider requests use the shared resource-policy sanitizer at the final model boundary, and privacy copy reflects the runtime behavior. Broader enterprise DLP, provider-retention posture, and non-resource prompt-secret detection remain a named post-RC hardening track.", "tracking": [ { "kind": "lane-followup", diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 6e9902405..514bcd1f4 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -387,10 +387,12 @@ prompt explain the same operator-facing priority. support diagnostics and logs without being shown a Pro-locked reporting tab, while paid instances keep the canonical `/settings/support/reporting` route and panel. - Data Handling is a trust surface, not a commercial surface. The Settings - shell must keep it under the Security group, route it through the canonical - registry/header/navigation model, and avoid trial, upgrade, paid-plan, or - monitoring-limit copy when commercial presentation is hidden. + Resource Privacy/Data Handling is a route-backed trust surface, not a + commercial surface or default settings destination. The Settings shell must + keep it governed by the Security registry/header/navigation model without + advertising it in the normal sidebar while it remains informational only, + and it must avoid trial, upgrade, paid-plan, or monitoring-limit copy when + commercial presentation is hidden. General settings runtime cards that present source-platform actions must consume `frontend-modern/src/utils/systemSettingsPresentation.ts` and the shared source-platform vocabulary rather than card-local product names. diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 58c3fca9a..7866fa3e1 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -138,7 +138,7 @@ controls as normal product settings. `pkg/tlsutil/fingerprint.go` together so startup auth ingestion, live auth-env reloads, hosted entitlement refresh origins, and pinned-fingerprint TLS clients keep one fail-closed security floor. -9. Change operator-facing data-handling posture through `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` and `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` together so resource classification, handling-boundary, and redaction copy stays governed as a trust surface. +9. Change operator-facing Resource Privacy/Data Handling posture through `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` and `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` together so resource classification, handling-boundary, redaction copy, and the route-backed/hidden-sidebar presentation stay governed as a trust surface. ## Forbidden Paths @@ -159,8 +159,8 @@ controls as normal product settings. hosted entitlement refresh origin handling, or pinned-certificate transport behavior changes. Hosted commercial URL overrides must remain absolute HTTP(S) URLs, with plain HTTP limited to loopback development origins. -8. Keep the Data Handling settings surface neutral and non-commercial: it may show resource policy posture, local-only counts, and redaction coverage, but it must not advertise trials, upgrades, paid plans, or monitoring limits. -9. Keep operator-facing Data Handling posture aligned with runtime AI/context enforcement: `local-only` resource details must not be sent to external model prompts, and sensitive free-form alert, tool-result, investigation, handoff context, and any retained legacy managed-model compatibility text must use the shared resource-policy redaction helper before leaving the local trust boundary. Assistant handoffs may surface canonical policy handling guidance and current resource-state summaries for product-originated resources, but that guidance and state are model-only context and must not become disclosure authority. Product-originated Assistant handoff text must also be policy-cleaned before prompt injection, including operator briefings and finding/action context, so raw governed resource identity cannot leak through local-model briefing prose while non-local transport still receives the final provider-bound sanitizer. All provider-bound AI requests to non-local models must use the shared resource-policy sanitizer immediately before transport so later agentic turns cannot bypass the advertised handling posture. +8. Keep the Resource Privacy/Data Handling settings surface neutral and non-commercial: it may show resource policy posture, local-only counts, and redaction coverage, but it must not advertise trials, upgrades, paid plans, or monitoring limits, and it must remain route-backed rather than promoted in the normal Settings sidebar while it is informational only. +9. Keep operator-facing Resource Privacy/Data Handling posture aligned with runtime AI/context enforcement: `local-only` resource details must not be sent to external model prompts, and sensitive free-form alert, tool-result, investigation, handoff context, and any retained legacy managed-model compatibility text must use the shared resource-policy redaction helper before leaving the local trust boundary. Assistant handoffs may surface canonical policy handling guidance and current resource-state summaries for product-originated resources, but that guidance and state are model-only context and must not become disclosure authority. Product-originated Assistant handoff text must also be policy-cleaned before prompt injection, including operator briefings and finding/action context, so raw governed resource identity cannot leak through local-model briefing prose while non-local transport still receives the final provider-bound sanitizer. All provider-bound AI requests to non-local models must use the shared resource-policy sanitizer immediately before transport so later agentic turns cannot bypass the advertised handling posture. 10. Keep the canonical and frontend-served privacy disclosures aligned with the actual AI transport boundary: self-managed installs must describe local providers as staying on the operator network, non-local providers as direct @@ -207,11 +207,13 @@ operator access. `frontend-modern/src/components/Settings/GeneralSettingsPanel.t and `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` must use monitoring/workspace wording for tours and read-only token presets instead of reviving Dashboard-specific labels after the Dashboard route has been retired. -The Data Handling settings surface extends that trust boundary to resource -policy posture. It may expose the canonical sensitivity, handling-boundary, -and redaction counts that Pulse already applies to resources, but it must stay -informational and non-commercial so free/self-hosted operators are not shown -paywall, trial, upgrade, or monitoring-limit prompts inside a privacy surface. +The Resource Privacy/Data Handling settings surface extends that trust boundary +to resource policy posture. It may expose the canonical sensitivity, +handling-boundary, and redaction counts that Pulse already applies to +resources, but it must stay informational, route-backed, hidden from the +normal Settings sidebar, and non-commercial so free/self-hosted operators are +not shown paywall, trial, upgrade, monitoring-limit prompts, or an empty +read-only destination inside a privacy surface. That posture is now enforced at the AI provider boundary too: non-local model requests must be sanitized from the same resource-policy metadata that powers the Data Handling surface. Assistant finding handoffs may hydrate policy diff --git a/frontend-modern/src/components/Settings/DataHandlingPanel.tsx b/frontend-modern/src/components/Settings/DataHandlingPanel.tsx index 20c6582c8..ff54786a8 100644 --- a/frontend-modern/src/components/Settings/DataHandlingPanel.tsx +++ b/frontend-modern/src/components/Settings/DataHandlingPanel.tsx @@ -2,12 +2,14 @@ import { For, Show, createMemo, type Component } from 'solid-js'; import AlertTriangle from 'lucide-solid/icons/alert-triangle'; import Cloud from 'lucide-solid/icons/cloud'; import EyeOff from 'lucide-solid/icons/eye-off'; +import Info from 'lucide-solid/icons/info'; import Lock from 'lucide-solid/icons/lock'; import RefreshCw from 'lucide-solid/icons/refresh-cw'; import ShieldCheck from 'lucide-solid/icons/shield-check'; import { useUnifiedResources } from '@/hooks/useUnifiedResources'; import Button from '@/components/shared/Button'; import SettingsPanel from '@/components/shared/SettingsPanel'; +import { settingsTabPath } from './settingsNavigationModel'; import { buildDataHandlingPanelModel, type DataHandlingPostureItem, @@ -31,6 +33,24 @@ const badgeClassByTone: Record = { const formatCount = (value: number): string => new Intl.NumberFormat().format(value); +const policyScopeItems = [ + { + label: 'Resource classification', + description: + 'Groups monitored resources by sensitivity so restricted infrastructure is not treated like routine inventory.', + }, + { + label: 'AI context boundary', + description: + 'Shows which resource details can be summarized externally, should prefer local handling, or must stay local.', + }, + { + label: 'Identifier redaction', + description: + 'Counts hostnames, IP addresses, platform IDs, aliases, and paths marked for removal from guarded views.', + }, +] as const; + const errorMessageFor = (error: unknown): string => { if (error instanceof Error && error.message.trim()) { return error.message; @@ -38,6 +58,58 @@ const errorMessageFor = (error: unknown): string => { return 'Unable to load resource policy posture.'; }; +const PolicyScopeSummary: Component = () => ( +
+
+
+
+ + {(item) => ( +
+

+ {item.label} +

+

{item.description}

+
+ )} +
+
+
+); + +const EmptyPolicyPostureState: Component = () => ( +
+
+
+

No monitored resources to classify

+

+ Pulse has not received canonical resource data for this scope yet. Once infrastructure + discovery has resources, this page will show which resource details stay local, which can + be summarized, and which identifiers are redacted. +

+

+ This is expected on a fresh instance, before discovery finishes, or when the selected + organization does not have visible monitored resources. +

+
+ + Open Infrastructure + +
+
+); + const PostureMeter: Component<{ item: DataHandlingPostureItem }> = (props) => (
@@ -73,7 +145,8 @@ export const DataHandlingPanel: Component = () => { return ( {
} > -
-
-
- - Governed Resources -
-

- {formatCount(model().totalResources)} -

-

Resources carrying policy metadata.

-
-
-
- - Local-Only -
-

- {formatCount(model().localOnlyResources)} -

-

Resources kept inside this Pulse instance.

-
-
-
- - Redaction Hints -
-

- {formatCount(model().redactionHintCount)} -

-

Field-level protections applied by policy.

-
-
+ - -

No governed resources yet

-

- Resource policy posture will appear after Pulse has canonical resource data. + }> +

+
+
+ + Governed Resources +
+

+ {formatCount(model().totalResources)}

+

Resources carrying policy metadata.

- } - > +
+
+ + Local-Only +
+

+ {formatCount(model().localOnlyResources)} +

+

Resources kept inside this Pulse instance.

+
+
+
+ + Redaction Hints +
+

+ {formatCount(model().redactionHintCount)} +

+

Field-level protections applied by policy.

+
+
+

Sensitivity

-

Classification applied to monitored resources.

+

+ Classification applied to monitored resources. +

{(item) => } diff --git a/frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx new file mode 100644 index 000000000..82cd3b1e2 --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/DataHandlingPanel.test.tsx @@ -0,0 +1,80 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, render, screen } from '@solidjs/testing-library'; +import { DataHandlingPanel } from '../DataHandlingPanel'; + +const unifiedResourcesState = vi.hoisted(() => ({ + error: null as unknown, + loading: false, + policyPosture: null as unknown, + refetch: vi.fn(), +})); + +vi.mock('@/hooks/useUnifiedResources', () => ({ + useUnifiedResources: () => ({ + error: () => unifiedResourcesState.error, + loading: () => unifiedResourcesState.loading, + policyPosture: () => unifiedResourcesState.policyPosture, + refetch: unifiedResourcesState.refetch, + }), +})); + +describe('DataHandlingPanel', () => { + beforeEach(() => { + unifiedResourcesState.error = null; + unifiedResourcesState.loading = false; + unifiedResourcesState.policyPosture = { + totalResources: 0, + sensitivityCounts: {}, + routingCounts: {}, + redactionCounts: {}, + }; + unifiedResourcesState.refetch.mockReset(); + }); + + afterEach(() => { + cleanup(); + }); + + it('explains the empty resource posture instead of leading with zero-value counters', () => { + render(() => ); + + expect(screen.getByText('Resource Data Policy')).toBeInTheDocument(); + expect(screen.getByText('Read-only resource privacy posture')).toBeInTheDocument(); + expect(screen.getByText('No monitored resources to classify')).toBeInTheDocument(); + expect(screen.getByText(/fresh instance, before discovery finishes/i)).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Open Infrastructure' })).toHaveAttribute( + 'href', + '/settings/infrastructure', + ); + expect(screen.queryByText('Governed Resources')).not.toBeInTheDocument(); + }); + + it('shows policy posture metrics once resources exist', () => { + unifiedResourcesState.policyPosture = { + totalResources: 4, + sensitivityCounts: { + internal: 1, + sensitive: 2, + restricted: 1, + }, + routingCounts: { + 'cloud-summary': 1, + 'local-first': 2, + 'local-only': 1, + }, + redactionCounts: { + hostname: 2, + 'ip-address': 1, + }, + }; + + render(() => ); + + expect(screen.getByText('Governed Resources')).toBeInTheDocument(); + expect(screen.getByText('Local-Only')).toBeInTheDocument(); + expect(screen.getByText('Redaction Hints')).toBeInTheDocument(); + expect(screen.getByText('Sensitivity')).toBeInTheDocument(); + expect(screen.getByText('Handling Boundary')).toBeInTheDocument(); + expect(screen.queryByText('No monitored resources to classify')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 83d4fb20a..2b970d763 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -170,6 +170,20 @@ describe('settings architecture guardrails', () => { ); }); + it('keeps resource privacy route-backed instead of sidebar-promoted', () => { + expect(settingsNavCatalogSource).toMatch( + /id: 'security-data-handling',[\s\S]*label: 'Resource Privacy',[\s\S]*hideFromSidebar: true/, + ); + expect(settingsHeaderMetaSource).toContain("title: 'Resource Privacy'"); + expect(settingsHeaderMetaSource).toContain( + 'See which monitored resource details can be summarized, must stay local, or are redacted.', + ); + expect(dataHandlingPanelSource).toContain('title="Resource Data Policy"'); + expect(dataHandlingPanelSource).toContain('Read-only resource privacy posture'); + expect(dataHandlingPanelSource).toContain(''); + expect(dataHandlingPanelSource).toContain(''); + }); + it('keeps default self-hosted commercial copy opt-in from shared settings primitives', () => { expect(aiSettingsDialogsSource).not.toContain('Open hosted handoff'); expect(aiSettingsDialogsSource).not.toContain( @@ -241,7 +255,7 @@ describe('settings architecture guardrails', () => { // The amber soft-warning tone is the operator's signal that the // provider accepted the request but the model did not call the tool. expect(aiModelSelectionSectionSource).toContain('model_tool_support_unverified'); - expect(aiModelSelectionSectionSource).not.toContain('fetch(\'/api/ai/patrol/preflight'); + expect(aiModelSelectionSectionSource).not.toContain("fetch('/api/ai/patrol/preflight"); }); it('hydrates the Patrol preflight panel from the cached settings snapshot', () => { @@ -256,16 +270,18 @@ describe('settings architecture guardrails', () => { expect(aiModelSelectionSectionSource).toContain('last verified'); }); - it('passes the form\'s pending patrolModel to runPatrolPreflight so Verify Patrol tests the unsaved selection', () => { + it("passes the form's pending patrolModel to runPatrolPreflight so Verify Patrol tests the unsaved selection", () => { // Without this, clicking Verify Patrol after changing the model // dropdown silently tested the previously-saved model and the // operator would believe their pending selection was verified. expect(aiSettingsStateSource).toContain('form.patrolModel'); expect(aiSettingsStateSource).toContain('pendingModel'); - expect(aiSettingsStateSource).toContain('runPatrolPreflight(pendingModel ? { model: pendingModel } : {})'); + expect(aiSettingsStateSource).toContain( + 'runPatrolPreflight(pendingModel ? { model: pendingModel } : {})', + ); }); - it('flags the inline preflight panel as stale when the cached result is for a different model than the form\'s current selection', () => { + it("flags the inline preflight panel as stale when the cached result is for a different model than the form's current selection", () => { // Cache may hold a green result for the previously-saved model // while the operator has changed the dropdown. Show a warning-tone // panel with copy that names both models so the green badge @@ -274,7 +290,9 @@ describe('settings architecture guardrails', () => { expect(aiModelSelectionSectionSource).toContain('pendingFormModel'); expect(aiModelSelectionSectionSource).toContain('cachedResultModel'); expect(aiModelSelectionSectionSource).toContain('Verified result is for'); - expect(aiModelSelectionSectionSource).toContain('Click Verify Patrol to test the pending selection'); + expect(aiModelSelectionSectionSource).toContain( + 'Click Verify Patrol to test the pending selection', + ); }); it('keeps contextual settings feature gates free of retired commercial telemetry wrappers', () => { diff --git a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx index 06ba8e2fa..e22e2b61c 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx @@ -116,8 +116,9 @@ describe('settingsNavigation integration scaffold', () => { expect(getSettingsNavItem('system-billing')?.hideFromSidebar).toBe(true); }); - it('keeps data handling visible as a non-commercial trust surface', () => { + it('keeps resource privacy route-backed but out of the normal sidebar', () => { expect(getSettingsNavItem('security-data-handling')?.hideWhenCommercialHidden).not.toBe(true); + expect(getSettingsNavItem('security-data-handling')?.hideFromSidebar).toBe(true); expect( shouldHideSettingsNavItem('security-data-handling', { hasFeature: hasFeatures([]), diff --git a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts index 568f714ac..018ae45ce 100644 --- a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts +++ b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts @@ -85,9 +85,9 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { description: 'View your security posture at a glance and monitor authentication status.', }, 'security-data-handling': { - title: 'Data Handling', + title: 'Resource Privacy', description: - 'Review resource classifications, handling boundaries, and redaction coverage.', + 'See which monitored resource details can be summarized, must stay local, or are redacted.', }, 'security-auth': { title: 'Authentication', diff --git a/frontend-modern/src/components/Settings/settingsNavCatalog.ts b/frontend-modern/src/components/Settings/settingsNavCatalog.ts index 8ebaa06e8..ddcd057c3 100644 --- a/frontend-modern/src/components/Settings/settingsNavCatalog.ts +++ b/frontend-modern/src/components/Settings/settingsNavCatalog.ts @@ -178,9 +178,10 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [ }, { id: 'security-data-handling', - label: 'Data Handling', + label: 'Resource Privacy', icon: FileText, iconProps: { strokeWidth: 2 }, + hideFromSidebar: true, }, { id: 'api',