diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 5c887d6e4..eecc0a55b 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -971,8 +971,10 @@ surplus width is redistributed across peer metric, source, uptime, and action columns instead of being dumped into the first column and wasting operator visible table density. That hot-path contract now includes policy badge rendering on resource rows. -Policy-rich table rows must collapse to a single inline policy summary chip -instead of stacking sensitivity and routing badges inside the resource column. +Policy-rich table rows must only surface a single inline summary chip for +blocking `local-only`/`restricted` posture; non-blocking `sensitive` + +`local-first` and redaction-only metadata belongs in Data Handling, detail, and +AI/governance surfaces instead of spending default row visual budget. Agentless availability evidence belongs on that same bounded row path. Infrastructure `network-endpoint` rows may replace otherwise empty host metric slots with one compact inline target/result readout, while the System column diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 38716fc97..2f9a1eacc 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -955,9 +955,12 @@ per-resource policy lines instead of reconstructing a separate The infrastructure host-table shell now treats the default `Internal` + `Cloud Summary` posture as canonical policy metadata that should stay available in the drawer and AI/governance surfaces without being promoted -to always-on row chrome. Inline row badges are reserved for non-default policy -states so the canonical resource surface does not imply that every host carries -an operator-actionable governance exception. +to always-on row chrome. Inline row badges are reserved for blocking policy +states such as `local-only` routing or `restricted` sensitivity, while +`sensitive` + `local-first` and redaction-only posture remains visible in Data +Handling, detail, and AI/governance surfaces. The table must not imply that +ordinary sensitive resources carry an operator-actionable infrastructure +exception. The resource drawer now applies the same rule to its investigation-context governance block: the default posture remains part of the canonical policy contract, but the drawer only surfaces the governance section when the policy @@ -1603,10 +1606,11 @@ performance proof route. The shared resource table and resource drawer now surface compact timeline summary chips, so facet presentation changes must continue to flow through the same governed resource-row surface rather than inventing a separate ad hoc summary path. Dense table rows must bound those -chips with an explicit visible limit and overflow label, while policy-rich -table rows collapse sensitivity/routing posture into a single inline summary -chip so mock-rich canonical resources cannot stack badges inside the resource -column. Those row summaries now prefer canonical `facetCounts` on the +chips with an explicit visible limit and overflow label, while row-level +policy chips are limited to blocking `local-only`/`restricted` posture so +mock-rich canonical resources cannot stack or overstate governance badges +inside the resource column. Those row summaries now prefer canonical +`facetCounts` on the resource object when available, so the backend list/read shapes remain the source of truth instead of forcing the frontend to infer totals only from loaded slices. The drawer now fetches those facets diff --git a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx index a451eab88..9f477b995 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx @@ -659,7 +659,7 @@ describe('UnifiedResourceTable performance contract', () => { expect(matchesSearch(governedResource, 'secret-host-9')).toBe(false); }); - it('suppresses the default policy posture badges in host-table rows while preserving exceptional policy badges', async () => { + it('suppresses non-blocking policy posture in host-table rows while preserving blocking policy badges', async () => { const resources = [ makeResource(0, { name: 'default-policy-host', @@ -677,6 +677,14 @@ describe('UnifiedResourceTable performance contract', () => { routing: { scope: 'local-first', redact: ['hostname'] }, }, }), + makeResource(2, { + name: 'local-only-host', + displayName: 'Local Only Host', + policy: { + sensitivity: 'restricted', + routing: { scope: 'local-only', redact: ['hostname'] }, + }, + }), ]; const { container } = render(() => ( @@ -698,10 +706,11 @@ describe('UnifiedResourceTable performance contract', () => { expect(bodyQueries.queryByText('Internal')).not.toBeInTheDocument(); expect(bodyQueries.queryByText('Cloud Summary')).not.toBeInTheDocument(); - const policyBadge = bodyQueries.getByText('Sensitive'); - expect(policyBadge).toBeInTheDocument(); - expect(policyBadge.closest('[title]')?.getAttribute('title')).toContain('Local First'); + expect(bodyQueries.queryByText('Sensitive')).not.toBeInTheDocument(); expect(bodyQueries.queryByText('Local First')).not.toBeInTheDocument(); + const policyBadge = bodyQueries.getByText('Local Only'); + expect(policyBadge).toBeInTheDocument(); + expect(policyBadge.closest('[title]')?.getAttribute('title')).toContain('Restricted'); }); it('renders facet summary badges without changing the Profile S row budget', async () => { @@ -935,7 +944,7 @@ describe('UnifiedResourceTable performance contract', () => { expect(rowCount).toBeLessThanOrEqual(140); }); await waitFor(() => { - expect(getAllByText('Restricted').length).toBeGreaterThan(0); + expect(getAllByText('Local Only').length).toBeGreaterThan(0); }); expect(getPreferredInfrastructureDisplayName(resources[0]!)).toBe('Sensitive Host'); }); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.policy-governance.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.policy-governance.test.tsx index e26162102..90a471152 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.policy-governance.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.policy-governance.test.tsx @@ -58,7 +58,7 @@ const resource: Resource = { }; describe('UnifiedResourceTable governance presentation', () => { - it('surfaces canonical policy badges in the resource row', () => { + it('surfaces blocking policy posture in the resource row', () => { const { getByText, queryByText } = render(() => ( { /> )); - expect(getByText('Restricted')).toBeInTheDocument(); expect(getByText('Local Only')).toBeInTheDocument(); + expect(queryByText('Restricted')).toBeNull(); expect(getByText('Sensitive Host')).toBeInTheDocument(); expect(queryByText('restricted host summary safe for remote AI consumption')).toBeNull(); expect(queryByText('(sensitive-host)')).toBeNull(); }); it('surfaces resource facet counts in the resource row', () => { + const facetResource: Resource = { + ...resource, + policy: { + sensitivity: 'sensitive', + routing: { + scope: 'local-first', + redact: ['hostname'], + }, + }, + }; + const { getByText, queryByText } = render(() => ( { ).toEqual(['Hostname', 'IP Address']); }); - it('suppresses the default internal cloud-summary posture in table rows', () => { + it('keeps non-blocking policy posture out of table rows', () => { expect( hasDefaultResourcePolicyPosture({ sensitivity: 'internal', @@ -57,14 +58,14 @@ describe('resourcePolicyPresentation utils', () => { ).toEqual([]); expect( - getResourcePolicyTableBadges({ + hasBlockingResourcePolicyPosture({ sensitivity: 'sensitive', routing: { scope: 'local-first', redact: ['hostname'], }, - }).map((badge) => badge.label), - ).toEqual(['Sensitive']); + }), + ).toBe(false); expect( getResourcePolicyTableBadges({ sensitivity: 'sensitive', @@ -72,8 +73,8 @@ describe('resourcePolicyPresentation utils', () => { scope: 'local-first', redact: ['hostname'], }, - })[0]?.title, - ).toContain('Local First'); + }).map((badge) => badge.label), + ).toEqual([]); expect( getResourcePolicyTableBadges({ sensitivity: 'internal', @@ -82,7 +83,7 @@ describe('resourcePolicyPresentation utils', () => { redact: ['hostname'], }, }).map((badge) => badge.label), - ).toEqual(['Redacted']); + ).toEqual([]); expect( hasDefaultResourcePolicyPosture({ @@ -95,6 +96,30 @@ describe('resourcePolicyPresentation utils', () => { ).toBe(false); }); + it('surfaces blocking local-only policy posture in table rows', () => { + expect( + hasBlockingResourcePolicyPosture({ + sensitivity: 'restricted', + routing: { + scope: 'local-only', + redact: ['hostname'], + }, + }), + ).toBe(true); + + const badges = getResourcePolicyTableBadges({ + sensitivity: 'restricted', + routing: { + scope: 'local-only', + redact: ['hostname'], + }, + }); + + expect(badges.map((badge) => badge.label)).toEqual(['Local Only']); + expect(badges[0]?.title).toContain('Restricted'); + expect(badges[0]?.title).toContain('Redacts Hostname'); + }); + it('uses concise governed labels for redacted resources', () => { expect( getResourcePolicyDisplayLabel({ diff --git a/frontend-modern/src/utils/resourcePolicyPresentation.ts b/frontend-modern/src/utils/resourcePolicyPresentation.ts index 3a5b2ea63..894b54400 100644 --- a/frontend-modern/src/utils/resourcePolicyPresentation.ts +++ b/frontend-modern/src/utils/resourcePolicyPresentation.ts @@ -95,12 +95,6 @@ const routingPresentation: Record< }, }; -const redactionBadgePresentation: Pick = { - label: 'Redacted', - title: 'Resource identity fields are redacted before governed handling.', - className: `${badgeBaseClass} bg-slate-200 text-slate-700 dark:bg-slate-800 dark:text-slate-300`, -}; - const redactionLabels: Record = { hostname: 'Hostname', 'ip-address': 'IP Address', @@ -122,25 +116,22 @@ export const hasDefaultResourcePolicyPosture = (policy?: ResourcePolicy): boolea (policy.routing.redact?.length ?? 0) === 0, ); +export const hasBlockingResourcePolicyPosture = (policy?: ResourcePolicy): boolean => + Boolean(policy && (policy.sensitivity === 'restricted' || policy.routing.scope === 'local-only')); + export const getResourcePolicyTableBadges = ( policy?: ResourcePolicy, ): PolicyBadgePresentation[] => { if (!policy) return []; - if (hasDefaultResourcePolicyPosture(policy)) { + if (!hasBlockingResourcePolicyPosture(policy)) { return []; } const sensitivity = sensitivityPresentation[policy.sensitivity]; const routing = routingPresentation[policy.routing.scope]; const redactions = getResourcePolicyRedactionLabels(policy); - const hasNonDefaultSensitivity = policy.sensitivity !== 'internal'; - const hasNonDefaultRouting = policy.routing.scope !== 'cloud-summary'; - const primary = hasNonDefaultSensitivity - ? sensitivity - : hasNonDefaultRouting - ? routing - : redactionBadgePresentation; + const primary = policy.routing.scope === 'local-only' ? routing : sensitivity; const redactionTitle = redactions.length > 0 ? `Redacts ${redactions.join(', ')}.` : undefined; return [