docs(branding): complete subsystem governance evidence

This commit is contained in:
courtmanr@gmail.com 2026-07-30 16:30:18 +01:00
parent 672db9113f
commit c700454b18
6 changed files with 65 additions and 0 deletions

View file

@ -5614,3 +5614,13 @@ callback builder already applied, and they neither mutate nor persist
update targeting, or fleet-lifecycle authority is touched, and the
extension-point expectations on `internal/api/` and `internal/api/router.go`
are otherwise unchanged.
### Runtime branding stays outside agent lifecycle authority
The authenticated `GET /api/runtime/branding` presentation endpoint is an
application-shell read, not an agent registration, configuration, update, or
command route. It may read the active `white_label` entitlement and the
tenant-local `reportBranding` settings record, but it must not enumerate
agents, resolve install tokens, mutate agent state, or reuse agent lifecycle
authority. Agent setup and reporting remain independent of whether an
operator has configured a custom application name or logo.

View file

@ -6832,3 +6832,10 @@ that chrome. Assistant and Patrol shell state stays owned by
`frontend-modern/src/stores/aiChat.ts` and the canonical Patrol path as before;
this containment is a shell-level authority rule and must not be forked into a
second AI-side scope check.
Application branding in `frontend-modern/src/AppLayout.tsx` is presentation
only. A licensed custom name or logo may replace the default Pulse lockup and
browser-title suffix, but it must not rename Pulse Assistant, alter Assistant
page context, change tool/provider identity, or enter prompt, transcript, or
Patrol evidence payloads. The AI runtime continues to consume canonical route
and resource context independently of the shell's displayed brand.

View file

@ -3233,3 +3233,14 @@ global banner block in `frontend-modern/src/App.tsx` (security, demo,
commercial migration, update, what's-new, GitHub star) must be gated on the
same predicate because those banners deep-link into settings destinations a
scope-limited session cannot open.
The authenticated shell may project tenant-local application branding only
through the narrow `GET /api/runtime/branding` response after normal session
and `monitoring:read` admission. The runtime must return
`enabled=false` with empty presentation values unless the active license grants
`white_label`; persisted `reportBranding` values must never become an
unlicensed browser-side feature flag. `frontend-modern/src/AppLayout.tsx` may
consume that filtered response for the header and document title, while
`frontend-modern/src/useAppRuntimeState.ts` keeps the fetch inside the existing
authenticated bootstrap and does not add a commercial-posture, checkout, or
organization probe.

View file

@ -2102,3 +2102,15 @@ manipulation on the request already in hand. They add no inventory scan,
persistence read, lock acquisition, metrics fan-out, or background work, and
their cost is constant per SSO provider entry rendered in the admin settings
response.
### Runtime branding remains a bounded bootstrap read
Authenticated application branding is loaded once alongside the existing
system-settings and layout bootstrap work. `useAppRuntimeState.ts` must keep
that request in the same parallel bootstrap group rather than serializing it
ahead of state hydration, organization startup, or WebSocket connection.
`GET /api/runtime/branding` performs one cached entitlement decision and at
most one tenant-local system-settings read; it must not scan tenants, read
external logo files, call a licensing network service, or add per-route and
per-component refetches. The response remains bounded to an enabled flag, one
120-character display name, and the already size-limited inline image.

View file

@ -620,4 +620,17 @@ describe('App architecture', () => {
expect(appLayoutSource).toContain("label: 'Patrol'");
expect(appLayoutSource).toContain('countLabel: patrolAttentionCountLabel()');
});
it('keeps licensed application branding inside the authenticated shell bootstrap', () => {
expect(appRuntimeStateSource).toContain(
'await Promise.all([loadSystemSettingsAndLayout(), loadRuntimeBranding()]);',
);
expect(appLayoutSource).toContain("import { runtimeBranding } from '@/stores/systemSettings';");
expect(appLayoutSource).toContain(
"const browserBrandName = createMemo(() => customBrandName() || 'Pulse');",
);
expect(appLayoutSource).toContain('data-testid="custom-brand-logo"');
expect(appRuntimeStateSource).not.toContain('/api/license/commercial-posture');
expect(appRuntimeStateSource).not.toContain('/api/license/entitlements');
});
});

View file

@ -14,6 +14,7 @@ import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSect
import aiRuntimeControlsSectionSource from '../AIRuntimeControlsSection.tsx?raw';
import aiSettingsModelSource from '../aiSettingsModel.ts?raw';
import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw';
import brandingSettingsCardSource from '../BrandingSettingsCard.tsx?raw';
import dockerRuntimeSettingsCardSource from '../DockerRuntimeSettingsCard.tsx?raw';
import settingsHeaderMetaSource from '../settingsHeaderMeta.ts?raw';
import settingsNavCatalogSource from '../settingsNavCatalog.ts?raw';
@ -1781,6 +1782,17 @@ describe('settings architecture guardrails', () => {
expect(diagnosticsResultsPanelSource).not.toContain('getStatusIndicatorBadgeToneClasses');
});
it('keeps application branding in the canonical Appearance settings surface', () => {
expect(generalSettingsPanelSource).toContain(
"import { BrandingSettingsCard } from './BrandingSettingsCard';",
);
expect(generalSettingsPanelSource).toContain('<BrandingSettingsCard');
expect(brandingSettingsCardSource).toContain("when={hasFeature('white_label')}");
expect(brandingSettingsCardSource).toContain('<FeatureGateSection');
expect(brandingSettingsCardSource).toContain('maxlength={120}');
expect(brandingSettingsCardSource).toContain('BRAND_LOGO_MAX_BYTES = 36 * 1024');
});
it('keeps every PBS diagnostic failure string inside the export redaction boundary', () => {
// Behavioural guard, not a source scan: the redaction gap that shipped was
// a field added to the payload after the sanitizer was written, which no