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',