diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index 0be504a0e..573127e3f 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -432,6 +432,11 @@ needs them, but `/api/license/commercial-posture`,
public demo mode and those lifecycle flows must not depend on licensed
identity, plan labels, upgrade reasons, checkout handoff state, or observed
usage counts surviving the public-demo contract.
+That same demo-hidden API boundary also keeps runtime-admin operations out of
+public lifecycle flows: `/api/diagnostics`,
+`/api/diagnostics/docker/prepare-token`, and `/api/logs/*` must return `404`
+in demo mode instead of exposing runtime bundles, log streams, or diagnostics
+payloads through a nominally read-only preview account.
Lifecycle-owned browser shells must also defer any commercial helper reads
until that presentation policy resolves so demo suppression stays fail-closed
during first render instead of racing hidden commercial endpoints from shared
diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md
index a8c6f5b9a..439b77624 100644
--- a/docs/release-control/v6/internal/subsystems/ai-runtime.md
+++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md
@@ -228,6 +228,10 @@ organization chrome: `frontend-modern/src/App.tsx` and
`frontend-modern/src/AppLayout.tsx` may hide org switchers or demo-only org
labels, but they must not couple assistant visibility, session reset, or
drawer-open behavior to that organization presentation state.
+That same shell boundary also owns demo-only Operations suppression:
+`frontend-modern/src/AppLayout.tsx` may remove the top-level Operations route
+from the public demo shell, but assistant availability and reset behavior must
+stay independent of that utility-tab presentation choice.
Authenticated `/login` recovery belongs to that same route shell boundary:
once login succeeds, `frontend-modern/src/App.tsx` must resolve `/login`
through the canonical post-auth landing route instead of leaving the
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 6bf0850cb..80dbc93af 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -1348,6 +1348,12 @@ That same diagnostics boundary must also backfill canonical fallback reasons
when a raw snapshot reaches the API layer without one, so
`buildMemorySourceDiagnostics` stays self-consistent even if a caller bypasses
`GetDiagnosticSnapshots()` and hands diagnostics a legacy alias directly.
+That same public-demo API boundary must also hide runtime-admin operations
+surfaces instead of treating them as harmless reads. Demo sessions must receive
+`404` for `/api/diagnostics`, `/api/diagnostics/docker/prepare-token`, and the
+shared `/api/logs/*` endpoints, so the preview shell cannot expose runtime
+diagnostics, log streams, or downloadable log bundles behind a supposedly
+read-only demo account.
That shared `InfrastructureOperationsController.tsx` boundary now also preserves copied shell
command payload continuity: any privilege-escalation wrapper applied at the
settings surface must keep the full canonical installer argument list intact
diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md
index 8658e1117..7eb44ba06 100644
--- a/docs/release-control/v6/internal/subsystems/cloud-paid.md
+++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md
@@ -228,6 +228,10 @@ trial urgency, observed usage counts, or checkout handoff state. The
commercial posture store and billing-entitlements store must also fail closed
locally until the shared presentation policy resolves, then stay fail-closed
in demo mode so hidden routes are not probed from the browser shell.
+That same browser-shell boundary also owns utility-nav suppression:
+`frontend-modern/src/AppLayout.tsx` must drop the top-level Operations tab in
+public demo mode instead of leaving diagnostics or system-log shells
+discoverable after commercial surfaces are hidden.
Deep-linkable commercial panels must consume the same resolved presentation
policy directly, not rely only on settings navigation to keep public-demo
browsers away from commercial routes. `ProLicensePanel` may instantiate its
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 43747c9d3..c51c58fdc 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -179,6 +179,11 @@ work extends shared components instead of creating new local variants.
posture: `/alerts` may continue exposing reporting tabs such as overview and
history, but activation controls plus configuration routes must collapse out
of the public-demo shell instead of advertising blocked management actions.
+ That same public-demo presentation boundary also owns top-level Operations
+ posture: the authenticated demo shell must not advertise the Operations
+ utility tab, and `/operations` deep links must hand back to the dashboard
+ instead of surfacing diagnostics or system-log chrome that the backend hides
+ for demo sessions.
3. Add feature-specific presentation only when no shared primitive should own it
4. Add guardrail tests when a new shared pattern is introduced
5. Keep shared platform-connections shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`, `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, and `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source instead of creating provider-local summary fetches or VMware-only shell vocabulary.
@@ -1183,7 +1188,11 @@ the tabbed operations surface, and
`frontend-modern/src/features/operations/operationsPageModel.ts` owns the tab
and path contract. The operations route must keep its navigation routed through
the shared `frontend-modern/src/components/shared/Subtabs.tsx` primitive rather
-than rebuilding a bespoke page-local tab bar.
+than rebuilding a bespoke page-local tab bar. When the session presentation
+policy marks the operator as a public demo viewer, that same route owner must
+suppress the surface entirely and hand the browser back to the canonical
+dashboard route instead of rendering diagnostics, reporting, or logs shells
+that are unavailable in demo mode.
The dashboard overview route now follows that same feature-owner pattern for
its dashboard-specific summary surfaces. `frontend-modern/src/pages/Dashboard.tsx`
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 8dffe94b4..e83122338 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -104,6 +104,11 @@ querying, and the operator-facing storage health presentation layer.
payloads must read through `GetUnifiedReadStateOrSnapshot()` so storage and
recovery consumers stay aligned with the canonical mock unified snapshot
instead of slipping onto the live store graph.
+ That same `internal/api/` demo boundary must keep runtime-admin operations
+ hidden from public preview sessions: `/api/diagnostics`,
+ `/api/diagnostics/docker/prepare-token`, and `/api/logs/*` must not remain
+ readable side channels while storage or recovery demo routes are otherwise
+ presented as read-only product surfaces.
Storage and recovery consumers must also inherit the hook's canonical
`ResourceType` normalization for route/query filters, so storage subtypes
such as `physical_disk` stay on the same cache-backed snapshot instead of
diff --git a/frontend-modern/src/AppLayout.tsx b/frontend-modern/src/AppLayout.tsx
index d619c1ce6..c85c8a390 100644
--- a/frontend-modern/src/AppLayout.tsx
+++ b/frontend-modern/src/AppLayout.tsx
@@ -40,7 +40,10 @@ import { getKioskModePreference, setKioskMode } from '@/utils/url';
import { updateStore } from '@/stores/updates';
import { aiChatStore } from '@/stores/aiChat';
import { isPro } from '@/stores/licenseCommercial';
-import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
+import {
+ presentationPolicyHidesUpgradePrompts,
+ presentationPolicyIsDemoMode,
+} from '@/stores/sessionPresentationPolicy';
import type { AppConnectionStatus } from '@/useAppRuntimeState';
const ROOT_INFRASTRUCTURE_PATH = buildInfrastructurePath();
@@ -422,7 +425,10 @@ export function AppLayout(props: AppLayoutProps) {
breakdown: undefined,
icon: ,
},
- {
+ ];
+
+ if (!presentationPolicyIsDemoMode()) {
+ tabs.push({
id: 'operations',
label: 'Operations',
route: '/operations',
@@ -431,8 +437,8 @@ export function AppLayout(props: AppLayoutProps) {
count: undefined,
breakdown: undefined,
icon: ,
- },
- ];
+ });
+ }
if (hasSettingsAccess) {
tabs.push({
diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts
index b51231dbb..0012d4d1d 100644
--- a/frontend-modern/src/__tests__/App.architecture.test.ts
+++ b/frontend-modern/src/__tests__/App.architecture.test.ts
@@ -75,6 +75,8 @@ describe('App architecture', () => {
expect(appLayoutSource).not.toContain('sessionPresentationPolicyResolved');
expect(appLayoutSource).not.toContain('presentationPolicyHidesCommercialSurfaces');
expect(appLayoutSource).not.toContain('presentationPolicyHidesOrganizationSurfaces');
+ expect(appLayoutSource).toContain('presentationPolicyIsDemoMode');
+ expect(appLayoutSource).toContain("if (!presentationPolicyIsDemoMode()) {");
expect(appLayoutSource).toContain(
'aiChatStore.enabled === true && !aiChatStore.isOpenSignal() && !kioskMode()',
);
diff --git a/frontend-modern/src/features/operations/OperationsPageSurface.tsx b/frontend-modern/src/features/operations/OperationsPageSurface.tsx
index 524e80337..b205fc544 100644
--- a/frontend-modern/src/features/operations/OperationsPageSurface.tsx
+++ b/frontend-modern/src/features/operations/OperationsPageSurface.tsx
@@ -1,4 +1,4 @@
-import { Suspense, createMemo, type Component, type JSX } from 'solid-js';
+import { Show, Suspense, createEffect, createMemo, type Component, type JSX } from 'solid-js';
import { useLocation, useNavigate } from '@solidjs/router';
import ActivityIcon from 'lucide-solid/icons/activity';
import FileTextIcon from 'lucide-solid/icons/file-text';
@@ -7,9 +7,12 @@ import { DiagnosticsPanel } from '@/components/Settings/DiagnosticsPanel';
import { ReportingPanel } from '@/components/Settings/ReportingPanel';
import { SystemLogsPanel } from '@/components/Settings/SystemLogsPanel';
import { Subtabs, type SubtabOption } from '@/components/shared/Subtabs';
+import { DASHBOARD_PATH } from '@/routing/resourceLinks';
+import { presentationPolicyIsDemoMode } from '@/stores/sessionPresentationPolicy';
import {
buildOperationsPath,
getOperationsTabFromPath,
+ operationsSurfaceHiddenInDemoMode,
OPERATIONS_TABS,
type OperationsTabId,
} from '@/features/operations/operationsPageModel';
@@ -24,21 +27,32 @@ export function OperationsPageSurface() {
const location = useLocation();
const navigate = useNavigate();
+ const hiddenInDemoMode = createMemo(() =>
+ operationsSurfaceHiddenInDemoMode(presentationPolicyIsDemoMode()),
+ );
const activeTab = createMemo(() => getOperationsTabFromPath(location.pathname));
+ createEffect(() => {
+ if (hiddenInDemoMode()) {
+ navigate(DASHBOARD_PATH, { replace: true });
+ }
+ });
+
const tabs = createMemo(() =>
- OPERATIONS_TABS.map((tab) => {
- const Icon = operationsTabIcons[tab.id];
- return {
- value: tab.id,
- label: (
-
-
- {tab.label}
-
- ) satisfies JSX.Element,
- };
- }),
+ hiddenInDemoMode()
+ ? []
+ : OPERATIONS_TABS.map((tab) => {
+ const Icon = operationsTabIcons[tab.id];
+ return {
+ value: tab.id,
+ label: (
+
+
+ {tab.label}
+
+ ) satisfies JSX.Element,
+ };
+ }),
);
const handleTabChange = (tabId: string) => {
@@ -46,33 +60,35 @@ export function OperationsPageSurface() {
};
return (
-