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.
This commit is contained in:
rcourtman
2026-05-13 23:52:06 +01:00
parent 2be14562ee
commit 649a601dca
9 changed files with 236 additions and 65 deletions
+1 -1
View File
@@ -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",
@@ -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.
@@ -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
@@ -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<DataHandlingPostureItem['tone'], string> = {
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 = () => (
<section class="rounded-md border border-sky-200 bg-sky-50 px-4 py-4 dark:border-sky-900 dark:bg-sky-950/40">
<div class="flex items-start gap-3">
<Info class="mt-0.5 h-4 w-4 shrink-0 text-sky-700 dark:text-sky-300" aria-hidden="true" />
<div class="min-w-0">
<h3 class="text-sm font-semibold text-base-content">Read-only resource privacy posture</h3>
<p class="mt-1 text-sm text-muted">
Pulse derives this from the unified resource registry. It does not configure retention,
backups, credentials, or billing data.
</p>
</div>
</div>
<div class="mt-4 grid gap-3 lg:grid-cols-3">
<For each={policyScopeItems}>
{(item) => (
<div class="rounded-md border border-border bg-surface px-3 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-base-content">
{item.label}
</p>
<p class="mt-1 text-sm text-muted">{item.description}</p>
</div>
)}
</For>
</div>
</section>
);
const EmptyPolicyPostureState: Component = () => (
<section class="rounded-md border border-border bg-surface-alt px-4 py-5">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="max-w-3xl">
<p class="text-sm font-semibold text-base-content">No monitored resources to classify</p>
<p class="mt-2 text-sm text-muted">
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.
</p>
<p class="mt-2 text-sm text-muted">
This is expected on a fresh instance, before discovery finishes, or when the selected
organization does not have visible monitored resources.
</p>
</div>
<a
href={settingsTabPath('infrastructure-systems')}
class="inline-flex w-fit items-center justify-center rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
>
Open Infrastructure
</a>
</div>
</section>
);
const PostureMeter: Component<{ item: DataHandlingPostureItem }> = (props) => (
<div class="rounded-md border border-border bg-surface-alt px-4 py-3">
<div class="flex items-start justify-between gap-3">
@@ -73,7 +145,8 @@ export const DataHandlingPanel: Component = () => {
return (
<SettingsPanel
title="Data Handling"
title="Resource Data Policy"
description="Review the current privacy boundary for monitored resource data."
bodyClass="space-y-5"
action={
<Button
@@ -119,55 +192,49 @@ export const DataHandlingPanel: Component = () => {
</div>
}
>
<div class="grid gap-3 md:grid-cols-3">
<div class="rounded-md border border-border bg-surface-alt p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-base-content">
<ShieldCheck class="h-4 w-4 text-emerald-600 dark:text-emerald-300" />
Governed Resources
</div>
<p class="mt-3 text-3xl font-semibold text-base-content">
{formatCount(model().totalResources)}
</p>
<p class="mt-1 text-xs text-muted">Resources carrying policy metadata.</p>
</div>
<div class="rounded-md border border-border bg-surface-alt p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-base-content">
<Lock class="h-4 w-4 text-teal-600 dark:text-teal-300" />
Local-Only
</div>
<p class="mt-3 text-3xl font-semibold text-base-content">
{formatCount(model().localOnlyResources)}
</p>
<p class="mt-1 text-xs text-muted">Resources kept inside this Pulse instance.</p>
</div>
<div class="rounded-md border border-border bg-surface-alt p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-base-content">
<EyeOff class="h-4 w-4 text-muted" />
Redaction Hints
</div>
<p class="mt-3 text-3xl font-semibold text-base-content">
{formatCount(model().redactionHintCount)}
</p>
<p class="mt-1 text-xs text-muted">Field-level protections applied by policy.</p>
</div>
</div>
<PolicyScopeSummary />
<Show
when={model().hasResources}
fallback={
<div class="rounded-md border border-border bg-surface-alt px-4 py-5">
<p class="text-sm font-semibold text-base-content">No governed resources yet</p>
<p class="mt-1 text-sm text-muted">
Resource policy posture will appear after Pulse has canonical resource data.
<Show when={model().hasResources} fallback={<EmptyPolicyPostureState />}>
<div class="grid gap-3 md:grid-cols-3">
<div class="rounded-md border border-border bg-surface-alt p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-base-content">
<ShieldCheck class="h-4 w-4 text-emerald-600 dark:text-emerald-300" />
Governed Resources
</div>
<p class="mt-3 text-3xl font-semibold text-base-content">
{formatCount(model().totalResources)}
</p>
<p class="mt-1 text-xs text-muted">Resources carrying policy metadata.</p>
</div>
}
>
<div class="rounded-md border border-border bg-surface-alt p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-base-content">
<Lock class="h-4 w-4 text-teal-600 dark:text-teal-300" />
Local-Only
</div>
<p class="mt-3 text-3xl font-semibold text-base-content">
{formatCount(model().localOnlyResources)}
</p>
<p class="mt-1 text-xs text-muted">Resources kept inside this Pulse instance.</p>
</div>
<div class="rounded-md border border-border bg-surface-alt p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-base-content">
<EyeOff class="h-4 w-4 text-muted" />
Redaction Hints
</div>
<p class="mt-3 text-3xl font-semibold text-base-content">
{formatCount(model().redactionHintCount)}
</p>
<p class="mt-1 text-xs text-muted">Field-level protections applied by policy.</p>
</div>
</div>
<div class="grid gap-4 xl:grid-cols-2">
<section class="space-y-3">
<div>
<h3 class="text-sm font-semibold text-base-content">Sensitivity</h3>
<p class="mt-1 text-xs text-muted">Classification applied to monitored resources.</p>
<p class="mt-1 text-xs text-muted">
Classification applied to monitored resources.
</p>
</div>
<div class="space-y-2">
<For each={model().sensitivityItems}>{(item) => <PostureMeter item={item} />}</For>
@@ -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(() => <DataHandlingPanel />);
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(() => <DataHandlingPanel />);
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();
});
});
@@ -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('<PolicyScopeSummary />');
expect(dataHandlingPanelSource).toContain('<EmptyPolicyPostureState />');
});
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', () => {
@@ -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([]),
@@ -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',
@@ -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',