Clarify Pulse Agent host profile support

Separate first-class platform support from Pulse Agent host profiles and classify Unraid as an agent-backed host profile while preserving it as presentation-only platform vocabulary.
This commit is contained in:
rcourtman
2026-05-07 22:28:24 +01:00
parent d9331d39e4
commit 7da942226a
33 changed files with 1040 additions and 69 deletions
@@ -9,6 +9,28 @@
"docker",
"kubernetes"
],
"agent_host_profiles": [
{
"id": "unraid",
"family": "Unraid",
"governance_state": "supported",
"readiness_stage": "supported",
"host_identity_tokens": [
"unraid"
],
"support_floor": {
"setup": "supported",
"visibility": "supported",
"workloads": "n/a",
"storage": "supported",
"recovery": "n/a",
"alerts": "supported",
"assistant_read": "supported",
"assistant_control": "read-only"
},
"storage_family": "onprem"
}
],
"platforms": [
{
"id": "agent",
@@ -33,6 +33,11 @@ through platform-by-platform improvisation.
intentionally merges API-backed and agent-backed truth.
7. Platform work must project into canonical shared resources first.
Do not add provider-local top-level resource types by default.
8. Agent host and appliance profiles are governed separately from first-class
platforms.
A profile may describe supported Pulse Agent deployment on a specific host
or appliance family, but it does not promote that family into
`PLATFORM_TYPE_KEYS` or first-class platform status.
## Platform Categories
@@ -84,6 +89,25 @@ Examples:
2. a unified agent on a TrueNAS appliance enriching a `truenas` system that is
already supported through the API-backed poller
### Agent Host Profile
An agent host profile is a governed compatibility profile for running Pulse
Agent on a specific host or appliance family.
It may reuse source/platform vocabulary for identity matching, but it is not a
platform id, does not carry `PLATFORM_TYPE_KEYS` membership, and does not
replace first-class platform support.
Agent host profiles must define:
1. canonical profile id
2. profile family or label
3. governance state
4. readiness stage
5. host identity tokens
6. support-floor row
7. optional storage family
## Canonical Ingestion Modes
### API-backed
@@ -221,6 +245,20 @@ Rules:
5. `azure`
6. `gcp`
Unraid remains presentation-only platform vocabulary so source/platform labels
stay available for compatibility, but its governed Pulse Agent support is
tracked below as an agent host profile instead of as a first-class platform.
### Agent Host Profiles
Support floor fields are recorded in this order: `setup`, `visibility`,
`workloads`, `storage`, `recovery`, `alerts`, `assistant_read`,
`assistant_control`.
| Profile | Family | Governance | Readiness | Host identity tokens | Storage family | Support floor |
| --- | --- | --- | --- | --- | --- | --- |
| `unraid` | `Unraid` | `supported` | `supported` | `unraid` | `onprem` | `setup=supported`; `visibility=supported`; `workloads=n/a`; `storage=supported`; `recovery=n/a`; `alerts=supported`; `assistant_read=supported`; `assistant_control=read-only` |
### Current support rows
Support floor fields are recorded in this order: `setup`, `visibility`,
@@ -248,13 +286,14 @@ paths, no canonical projections, and `n/a` for every support-floor field.
supported, admitted, and presentation-only platform vocabulary declared here,
plus the canonical platform-family, readiness-stage, primary-mode,
onboarding-path, projection, and support-floor classification for supported
and admitted platforms. Tests and shared frontend vocabulary may consume that
manifest, and the tracked frontend projection in
`frontend-modern/src/utils/platformSupportManifest.generated.ts` must be
generated from it, but neither projection may introduce platform ids or
governance states, platform families, readiness stages, primary modes,
onboarding paths, projections, or support-floor claims that are not declared in
this document.
and admitted platforms, and the separate machine-readable agent host profile
classification for supported host/appliance profiles. Tests and shared
frontend vocabulary may consume that manifest, and the tracked frontend
projection in `frontend-modern/src/utils/platformSupportManifest.generated.ts`
must be generated from it, but neither projection may introduce platform ids,
host profile ids, governance states, families, readiness stages, primary
modes, onboarding paths, projections, support-floor claims, or host identity
tokens that are not declared in this document.
### Runtime variants
@@ -175,6 +175,11 @@ those facts into source-strategy copy, but it must not turn an admitted
`first-lab-ready` platform such as VMware into a product-level supported
claim, invent platform-local projections, or classify assistant control beyond
the manifest's support-floor row.
That same lifecycle-owned helper must keep first-class platform APIs separate
from governed Pulse Agent host profiles. Host/appliance compatibility such as
Unraid is presented as an agent install/profile path sourced from the manifest
`agent_host_profiles` section, not as `PLATFORM_TYPE_KEYS` membership or a
peer API-backed platform.
The lifecycle-owned infrastructure source manager also owns platform/system
grouping as source-management content, but not its table band presentation:
`frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx` must
@@ -792,6 +792,12 @@ the canonical monitored-system blocked payload.
kernel, architecture, and command capability, so settings surfaces can
render recognizable standalone-host identity without a second inventory
fetch or frontend-local host reconciliation rules.
Appliance-specific Pulse Agent compatibility is an additive host-profile
fact on that same identity payload. For Unraid and similar host profiles,
`agentIdentity.platform` remains the canonical runtime platform such as
`linux`, while `agentIdentity.hostProfile` carries the governed profile id
such as `unraid`; frontend clients must not re-promote those profile ids
into first-class platform types.
`pulse fleet connections` may read that same `GET /api/connections`
payload as a deterministic CLI adapter for agent-ready operations, but it
must remain a read-only view over the canonical connections ledger rather
@@ -607,7 +607,7 @@ frontend primitive boundary.
must derive primary issue labels and summaries from explicit incidents,
storage risk summaries, or storage-risk reasons so healthy rows do not
render impact text as a warning.
8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed platform family, not the primary customer-facing label.
8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed platform family, not the primary customer-facing label. Agent host-profile entries, including Unraid, stay in the generated `agentHostProfiles` projection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union.
9. Keep top-of-page summary interaction on shared primitives. Infrastructure, workloads, and storage summary cards must route sticky-shell behavior through `frontend-modern/src/components/shared/StickySummarySection.tsx` and route row-hover or focused-series rendering through shared chart primitives such as `frontend-modern/src/components/shared/InteractiveSparkline.tsx` and `frontend-modern/src/components/shared/DensityMap.tsx`, rather than page-local sticky wrappers or metric-card-specific hover logic. When a page keeps summary charts visible below the desktop breakpoint, it must use the shared `stickyDesktopOnly` mode instead of adding page-local media queries, so wrapped two-column summaries scroll as normal content and only become sticky once the large-screen layout is active. The shared summary-card contract must also own stable summary-card geometry for chart-backed cards so row hover, focus, synchronized readouts, or idle header metadata cannot ratchet the sticky summary taller across rerenders.
10. Keep summary chart interaction identity on one shared helper. Summary surfaces that expose row-hover, group-hover, chart-hover, or route-focus-driven chart emphasis must derive page/group/entity scope through `frontend-modern/src/components/shared/summaryCardInteraction.ts` and pass that same resolved scope into card-state, sparkline, and density-map primitives, rather than letting cards read `hovered || focused` while charts listen to a different page-local ID source. Hovering one summary chart must promote that series into the shared active entity so sibling cards highlight the same object instead of keeping chart-local hover islands, and hovering or pinning a workload group header, infrastructure cluster header, or storage pool-group header must scope the matching summary cards through that same shared contract instead of forking a page-local summary filter path. Sibling cards should surface that synchronized hover as one compact header readout through the shared summary-card contract, while the chart under the pointer keeps the only floating tooltip. `frontend-modern/src/components/Recovery/RecoverySummary.tsx` is explicitly outside this interaction dialect: recovery posture cards may share summary framing, but they must not silently grow row/group/chart hover behavior without a separate governed product decision.
11. Keep page summaries page-scoped when table rows enter contextual focus. Route-backed row selection may add a focused label and shared series emphasis, but infrastructure, workloads, and storage summary cards must continue to render the page-level series set instead of collapsing the summary down to the selected row or replacing the global trend view with row-local empty states.
@@ -508,9 +508,11 @@ bypass the API fail-closed execution gate.
systems, duplicate recovery inventory rows, or a storage-local ownership
model. The same shared `/api/connections` contract also owns compact
`agentIdentity` facts for agent-backed rows; storage and recovery may read
that metadata when they need to label a represented host, but they must not
rebuild OS/endpoint identity from recovery inventory or alias heuristics.
taxonomy. When that grouped platform row is a Proxmox cluster, storage and
that metadata, including host-profile ids such as `unraid`, when they need
to label a represented host, but they must not rebuild OS/endpoint identity
from recovery inventory or alias heuristics, or reinterpret an agent
host-profile id as a storage provider platform.
When that grouped platform row is a Proxmox cluster, storage and
recovery must also treat the backend-authored cluster moniker as the
canonical row identity instead of re-expanding cluster-member agents into
sibling host rows or per-node storage owners. If the grouped row carries
@@ -611,7 +611,11 @@ must stay generated from
`frontend-modern/src/types/resource.ts` must derive `PlatformType` from that
generated supported-plus-admitted projection rather than hand-maintaining a
second platform union that can drift from the governed manifest or re-admit
presentation-only labels by mistake.
presentation-only labels by mistake. Agent host profiles are generated beside
that platform projection for shared identity/presentation use only; a profile
such as Unraid may label a Pulse Agent host, but it must not enter
`PlatformType`, `PLATFORM_TYPE_KEYS`, unified-resource source filters, or
canonical top-level platform identity.
That same shared source boundary also applies when unified seeds and
supplemental providers coexist. If a canonical unified-resource seed omits an
owned supplemental source such as TrueNAS or VMware, the shared resource API
@@ -165,7 +165,8 @@ describe('ConnectionsAPI', () => {
source: 'agent',
agentIdentity: {
hostname: 'tower',
platform: 'unraid',
platform: 'linux',
hostProfile: 'unraid',
osName: 'Unraid',
osVersion: '7.1.0',
kernelVersion: '6.12.0',
@@ -183,7 +184,8 @@ describe('ConnectionsAPI', () => {
expect(result.connections[0]).toMatchObject({
agentIdentity: {
hostname: 'tower',
platform: 'unraid',
platform: 'linux',
hostProfile: 'unraid',
osName: 'Unraid',
osVersion: '7.1.0',
kernelVersion: '6.12.0',
+1
View File
@@ -65,6 +65,7 @@ export interface ConnectionFleetGovernance {
export interface ConnectionAgentIdentity {
hostname?: string;
platform?: string;
hostProfile?: string;
osName?: string;
osVersion?: string;
kernelVersion?: string;
@@ -1,6 +1,7 @@
import { Component, For, Show } from 'solid-js';
import type { ProbeCandidate } from '@/api/connections';
import { formControl, formField, formHelpText, formLabel } from '@/components/shared/Form';
import { getInfrastructureAgentHostProfileSupportText } from '@/utils/infrastructureOnboardingPresentation';
import type { CompletedProbePhase, ConnectionEditorState } from './useConnectionEditor';
import { CONNECTION_TYPE_LABELS } from './useConnectionEditor';
@@ -70,7 +71,9 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
<div class="mt-1 text-xs">
<Show
when={props.onChooseSourceTypeInstead}
fallback={<span>Pick a supported product from the catalog below, or if this is </span>}
fallback={
<span>Pick a supported product from the catalog below, or if this is </span>
}
>
<button
type="button"
@@ -81,7 +84,7 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
</button>
<span>, or if this is </span>
</Show>
a Linux, macOS, Windows, FreeBSD, or Unraid host,{' '}
this is one of the supported {getInfrastructureAgentHostProfileSupportText()},{' '}
<Show
when={props.onInstallAgent}
fallback={<span class="font-medium">install Pulse Agent instead</span>}
@@ -6,7 +6,10 @@ import {
createConnectionEditorState,
type ConnectionEditorState,
} from './useConnectionEditor';
import { getInfrastructureAutoDetectLabels } from '@/utils/infrastructureOnboardingPresentation';
import {
getInfrastructureAgentHostProfileSupportText,
getInfrastructureAutoDetectLabels,
} from '@/utils/infrastructureOnboardingPresentation';
export type ConnectionEditorMode = 'add' | 'edit';
@@ -128,7 +131,7 @@ export const ConnectionEditor: Component<ConnectionEditorProps> = (props) => {
>
Install Pulse Agent
</button>{' '}
for Linux, macOS, Windows, FreeBSD, or Unraid hosts.
for {getInfrastructureAgentHostProfileSupportText()}.
</p>
<AddressProbeStep
@@ -2,6 +2,7 @@ import { Component, For, Show } from 'solid-js';
import { Activity, Archive, Cpu, Database, Mail, Search, Server, ServerCog } from 'lucide-solid';
import type { InfrastructureOnboardingConnectionType } from '@/utils/infrastructureOnboardingPresentation';
import {
getInfrastructureGovernanceBadgeLabel,
getInfrastructureSourcePickerGroups,
getInfrastructureOnboardingProductPresentation,
getInfrastructureSourceStrategyPresentation,
@@ -66,6 +67,10 @@ export const InfrastructureSourcePicker: Component<InfrastructureSourcePickerPro
const strategy = getInfrastructureSourceStrategyPresentation(
details.sourceStrategy,
);
const governanceBadge = getInfrastructureGovernanceBadgeLabel(
details.governanceState,
details.readinessStage,
);
return (
<button
type="button"
@@ -87,9 +92,9 @@ export const InfrastructureSourcePicker: Component<InfrastructureSourcePickerPro
<span class="inline-flex items-center rounded-full border border-border bg-surface-alt px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-base-content">
{strategy.label}
</span>
<Show when={details.governanceState === 'admitted'}>
<Show when={governanceBadge}>
<span class="inline-flex items-center rounded-full border border-blue-200 bg-blue-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-blue-800 dark:border-blue-900 dark:bg-blue-950/40 dark:text-blue-200">
Available now
{governanceBadge}
</span>
</Show>
</div>
@@ -0,0 +1,21 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render, screen } from '@solidjs/testing-library';
import { InfrastructureSourcePicker } from '../InfrastructureSourcePicker';
describe('InfrastructureSourcePicker', () => {
afterEach(() => {
cleanup();
});
it('labels admitted VMware as first-lab-ready and keeps Pulse Agent on the host profile path', () => {
render(() => <InfrastructureSourcePicker onSelectType={vi.fn()} />);
expect(screen.getByText('First lab ready')).toBeInTheDocument();
expect(screen.queryByText('Available now')).toBeNull();
expect(
screen.getByText(
'Linux, macOS, Windows, FreeBSD, and Unraid host/appliance profiles where you want low-overhead node-local telemetry.',
),
).toBeInTheDocument();
});
});
@@ -729,7 +729,8 @@ describe('InfrastructureWorkspace', () => {
agentVersion: '6.0.2',
agentIdentity: {
hostname: 'tower',
platform: 'unraid',
platform: 'linux',
hostProfile: 'unraid',
osName: 'Unraid',
osVersion: '7.1.0',
kernelVersion: '6.12.0',
@@ -536,7 +536,7 @@ describe('settings architecture guardrails', () => {
expect(addressProbeStepSource).toContain('Probe address');
expect(addressProbeStepSource).toContain('install Pulse Agent instead');
expect(addressProbeStepSource).toContain('Choose a source type instead');
expect(addressProbeStepSource).toContain('Linux, macOS, Windows, FreeBSD, or Unraid host');
expect(addressProbeStepSource).toContain('getInfrastructureAgentHostProfileSupportText');
expect(addressProbeStepSource).toContain('supported API-backed platform');
expect(connectionEditorStateSource).toContain('ConnectionsAPI.probe(value)');
@@ -35,7 +35,8 @@ describe('useConnectionsLedger', () => {
},
agentIdentity: {
hostname: 'tower',
platform: 'unraid',
platform: 'linux',
hostProfile: 'unraid',
osName: 'Unraid',
osVersion: '7.1.0',
kernelVersion: '6.12.0',
@@ -1,4 +1,4 @@
import type { Connection } from '@/api/connections';
import type { Connection, ConnectionAgentIdentity } from '@/api/connections';
import type { ConnectionType } from '@/api/connections';
import type {
ConnectionFleetAdapterHealth,
@@ -11,6 +11,7 @@ import type {
ConnectionFleetUpdateStatus,
ConnectionFleetVersionDrift,
} from '@/api/connections';
import { getAgentHostProfileFamily } from '@/utils/platformSupportManifest';
export const lastActivityTextFromLastSeen = (lastSeen?: string | null): string => {
if (!lastSeen) return 'No activity yet';
@@ -30,6 +31,10 @@ export const lastActivityTextFromLastSeen = (lastSeen?: string | null): string =
export const connectionLastActivityText = (connection: Connection): string =>
lastActivityTextFromLastSeen(connection.lastSeen);
type ConnectionAgentIdentityPresentation = ConnectionAgentIdentity & {
hostProfile?: string | null;
};
const prettifyPlatform = (platform?: string | null): string | null => {
const normalized = platform?.trim().toLowerCase();
if (!normalized) return null;
@@ -50,6 +55,16 @@ const prettifyPlatform = (platform?: string | null): string | null => {
}
};
const connectionAgentHostProfileLabel = (
identity?: ConnectionAgentIdentityPresentation | null,
): string | null => {
const hostProfile = identity?.hostProfile?.trim();
if (hostProfile) {
return getAgentHostProfileFamily(hostProfile) ?? prettifyPlatform(hostProfile);
}
return prettifyPlatform(identity?.platform ?? identity?.osName);
};
const isIPv4Literal = (value: string): boolean => /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value);
const firstDistinctAgentIPv4Alias = (connection: Connection): string | null => {
@@ -74,11 +89,13 @@ const firstDistinctAgentIPv4Alias = (connection: Connection): string | null => {
};
export const connectionAgentIdentitySummary = (connection: Connection): string | null => {
const hostProfile = connectionAgentHostProfileLabel(connection.agentIdentity);
const osName = connection.agentIdentity?.osName?.trim();
const osVersion = connection.agentIdentity?.osVersion?.trim();
if (hostProfile && osVersion) return `${hostProfile} ${osVersion}`;
if (osName && osVersion) return `${osName} ${osVersion}`;
if (osName) return osName;
return prettifyPlatform(connection.agentIdentity?.platform);
return hostProfile;
};
export const connectionAgentEndpointDisplay = (connection: Connection): string | null => {
@@ -31,7 +31,7 @@ export const CONNECTION_TYPE_LABELS: Record<ConnectionType, string> = {
vmware: 'VMware vCenter',
truenas: 'TrueNAS SCALE',
availability: 'Network Endpoint',
agent: 'Pulse Unified Agent',
agent: 'Pulse Agent',
docker: 'Docker',
kubernetes: 'Kubernetes',
};
@@ -1,10 +1,13 @@
import { describe, expect, it } from 'vitest';
import {
getInfrastructureAgentHostProfileSupportText,
getInfrastructureCoverageCompleteActionPresentation,
getInfrastructureApiProductsByGovernanceState,
getInfrastructureAutoDetectLabels,
getInfrastructureEmptyStateDetail,
getInfrastructureGovernanceBadgeLabel,
getInfrastructureEmptyStateSummary,
INFRASTRUCTURE_ONBOARDING_PATHS,
getInfrastructureOnboardingProductPresentation,
getInfrastructureSourceManagerProducts,
getInfrastructureSourcePickerGroups,
@@ -37,7 +40,11 @@ describe('infrastructureOnboardingPresentation', () => {
const agent = getInfrastructureOnboardingProductPresentation('agent');
const pve = getInfrastructureOnboardingProductPresentation('pve');
expect(agent.bestFor).toContain('full node-local telemetry');
expect(getInfrastructureAgentHostProfileSupportText()).toBe(
'Linux, macOS, Windows, FreeBSD, and Unraid host/appliance profiles',
);
expect(agent.bestFor).toContain('host/appliance profiles');
expect(agent.bestFor).toContain('low-overhead node-local telemetry');
expect(agent.coverage).toContain('Low-overhead host telemetry');
expect(agent.catalogDescription).toContain('Low-overhead host telemetry');
expect(agent.sourceStrategy).toBe('agent');
@@ -55,6 +62,8 @@ describe('infrastructureOnboardingPresentation', () => {
expect(getInfrastructureSourceStrategyPresentation('agent')).toMatchObject({
label: 'Agent telemetry',
summary: 'Pulse Agent',
detail:
'Installs Pulse Agent for Linux, macOS, Windows, FreeBSD, and Unraid host/appliance profiles, local services, Docker, and Kubernetes.',
});
expect(getInfrastructureSourceStrategyPresentation('api-agent')).toMatchObject({
label: 'API first',
@@ -73,6 +82,8 @@ describe('infrastructureOnboardingPresentation', () => {
primaryMode: 'api-backed',
canonicalProjections: ['network-endpoint'],
});
expect(INFRASTRUCTURE_ONBOARDING_PATHS.api.title).toBe('Connect platform API');
expect(INFRASTRUCTURE_ONBOARDING_PATHS.agent.title).toBe('Install Pulse Agent');
});
it('keeps supported API products separate from the admitted VMware path', () => {
@@ -89,6 +100,13 @@ describe('infrastructureOnboardingPresentation', () => {
expect(
getInfrastructureApiProductsByGovernanceState('admitted').map((product) => product.label),
).toEqual(['VMware vCenter']);
expect(
getInfrastructureGovernanceBadgeLabel(
getInfrastructureOnboardingProductPresentation('vmware').governanceState,
getInfrastructureOnboardingProductPresentation('vmware').readinessStage,
),
).toBe('First lab ready');
expect(getInfrastructureGovernanceBadgeLabel('supported', 'supported')).toBeNull();
});
it('derives picker groups, auto-detect copy, and landing summaries from the shared helper', () => {
@@ -9,6 +9,10 @@ import {
resolveSourceTypeFromSources,
} from '@/utils/sourcePlatforms';
import {
AGENT_HOST_PROFILE_IDS,
PLATFORM_TYPE_KEYS,
PRESENTATION_ONLY_PLATFORM_IDS,
getAgentHostProfileFamily,
getSourcePlatformCanonicalProjections,
getSourcePlatformReadinessStage,
getSourcePlatformSupportFloor,
@@ -139,6 +143,13 @@ describe('sourcePlatforms', () => {
assistantControl: 'read-only',
});
});
it('keeps Unraid as an agent host profile instead of a platform type', () => {
expect(AGENT_HOST_PROFILE_IDS).toEqual(['unraid']);
expect(getAgentHostProfileFamily('unraid')).toBe('Unraid');
expect(PLATFORM_TYPE_KEYS).not.toContain('unraid');
expect(PRESENTATION_ONLY_PLATFORM_IDS).toContain('unraid');
});
});
describe('resolvePlatformTypeFromSources', () => {
@@ -1,5 +1,6 @@
import type { ConnectionType } from '@/api/connections';
import {
SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES,
getSourcePlatformManifestEntry,
type PlatformGovernanceState,
type PlatformPrimaryMode,
@@ -72,6 +73,45 @@ export interface InfrastructureCoverageCompleteActionPresentation {
detail: string;
}
const INFRASTRUCTURE_AGENT_RUNTIME_PLATFORM_LABELS = [
'Linux',
'macOS',
'Windows',
'FreeBSD',
] as const;
const formatJoinedLabelList = (labels: readonly string[]): string => {
if (labels.length === 0) return '';
if (labels.length === 1) return labels[0];
if (labels.length === 2) return `${labels[0]} and ${labels[1]}`;
return `${labels.slice(0, -1).join(', ')}, and ${labels[labels.length - 1]}`;
};
const getInfrastructureAgentHostProfileLabels = (): readonly string[] => {
const labels = [
...INFRASTRUCTURE_AGENT_RUNTIME_PLATFORM_LABELS,
...SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES.filter(
(profile) => profile.governanceState === 'supported',
).map((profile) => profile.family),
];
return Array.from(new Set(labels));
};
export const getInfrastructureAgentHostProfileSupportText = (): string =>
`${formatJoinedLabelList(getInfrastructureAgentHostProfileLabels())} host/appliance profiles`;
export const getInfrastructureGovernanceBadgeLabel = (
governanceState: PlatformGovernanceState,
readinessStage: PlatformReadinessStage,
): string | null => {
if (governanceState === 'supported') return null;
if (governanceState === 'admitted') {
return readinessStage === 'first-lab-ready' ? 'First lab ready' : 'Admitted';
}
if (governanceState === 'presentation-only') return 'Presentation only';
return null;
};
const SOURCE_STRATEGY_PRESENTATION: Record<
InfrastructureSourceStrategy,
InfrastructureSourceStrategyPresentation
@@ -84,7 +124,7 @@ const SOURCE_STRATEGY_PRESENTATION: Record<
agent: {
label: 'Agent telemetry',
summary: 'Pulse Agent',
detail: 'Installs Pulse Agent for host telemetry, local services, Docker, and Kubernetes.',
detail: `Installs Pulse Agent for ${getInfrastructureAgentHostProfileSupportText()}, local services, Docker, and Kubernetes.`,
},
'api-agent': {
label: 'API first',
@@ -105,8 +145,7 @@ const PRODUCT_PRESENTATION: Record<
> = {
agent: {
label: 'Pulse Agent',
bestFor:
'Linux, macOS, Windows, FreeBSD, and compatible hosts such as Unraid. Recommended on each machine where you want full node-local telemetry.',
bestFor: `${getInfrastructureAgentHostProfileSupportText()} where you want low-overhead node-local telemetry.`,
coverage: 'Low-overhead host telemetry, SMART, services, Docker, and Kubernetes',
catalogDescription: 'Low-overhead host telemetry, services, Docker, Kubernetes',
sourceStrategy: 'agent',
@@ -232,7 +271,7 @@ export const INFRASTRUCTURE_ONBOARDING_PATHS: Record<
InfrastructureOnboardingPathPresentation
> = {
api: {
title: 'Connect a supported platform',
title: 'Connect platform API',
description:
'Use a management API when the platform exposes one. Pulse validates the endpoint, requests credentials, and then starts collecting platform inventory and health.',
bestFor: 'TrueNAS, Proxmox, and the current VMware vCenter integration path',
@@ -240,10 +279,8 @@ export const INFRASTRUCTURE_ONBOARDING_PATHS: Record<
},
agent: {
title: 'Install Pulse Agent',
description:
'Use the agent when you want low-overhead machine telemetry, or when the system does not expose a management API Pulse can connect to directly.',
bestFor:
'Linux, macOS, Windows, FreeBSD, and compatible hosts such as Unraid. Recommended on each machine where you want full node-local telemetry.',
description: `Use the agent when you want low-overhead machine telemetry for ${getInfrastructureAgentHostProfileSupportText()}, or when the system does not expose a management API Pulse can connect to directly.`,
bestFor: `${getInfrastructureAgentHostProfileSupportText()} where you want full node-local telemetry.`,
coverage:
'Low-overhead CPU temperature, disk SMART, services, network metrics, Docker, and Kubernetes telemetry',
},
@@ -255,13 +292,7 @@ export const INFRASTRUCTURE_AGENT_DISCOVERY_LABELS = [
'Kubernetes',
] as const;
export const INFRASTRUCTURE_AGENT_HOST_LABELS = [
'Linux',
'macOS',
'Windows',
'FreeBSD',
'Unraid',
] as const;
export const INFRASTRUCTURE_AGENT_HOST_LABELS = getInfrastructureAgentHostProfileLabels();
const SOURCE_PICKER_GROUPS: InfrastructureSourcePickerGroupPresentation[] = [
{
@@ -1,11 +1,11 @@
// This file is generated by scripts/release_control/generate_platform_support_frontend_module.py.
// Do not edit by hand.
// Source: docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json
// Source SHA256: 60952e07f032d1a607075c2163c44ffbd5144617fd2e04a5ff1a9ff78490c353
// Source SHA256: 48a9ef03ba53e39c676b8a3aaf3b608ffd4c2c0269c2e1e485170a006eadfe21
export const PLATFORM_SUPPORT_MANIFEST_SOURCE = {
path: 'docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json',
sha256: '60952e07f032d1a607075c2163c44ffbd5144617fd2e04a5ff1a9ff78490c353',
sha256: '48a9ef03ba53e39c676b8a3aaf3b608ffd4c2c0269c2e1e485170a006eadfe21',
} as const;
export const PLATFORM_SUPPORT_MANIFEST = {
schemaVersion: 1,
@@ -18,6 +18,26 @@ export const PLATFORM_SUPPORT_MANIFEST = {
'docker',
'kubernetes',
],
agentHostProfiles: [
{
id: 'unraid',
family: 'Unraid',
governanceState: 'supported',
readinessStage: 'supported',
hostIdentityTokens: ['unraid'],
supportFloor: {
setup: 'supported',
visibility: 'supported',
workloads: 'n/a',
storage: 'supported',
recovery: 'n/a',
alerts: 'supported',
assistantRead: 'supported',
assistantControl: 'read-only',
},
storageFamily: 'onprem',
},
],
platforms: [
{
id: 'agent',
@@ -358,6 +378,9 @@ export const PLATFORM_SUPPORT_MANIFEST = {
],
} as const;
export const SOURCE_PLATFORM_MANIFEST_ENTRIES = PLATFORM_SUPPORT_MANIFEST.platforms;
export const SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES =
PLATFORM_SUPPORT_MANIFEST.agentHostProfiles;
export const AGENT_HOST_PROFILE_IDS = ['unraid'] as const;
export const SUPPORTED_PLATFORM_IDS = [
'agent',
'docker',
@@ -473,6 +496,33 @@ export const SOURCE_PLATFORM_ONBOARDING_PATH_KEYS = [
'install-workspace',
'platform-connections',
] as const;
export const SOURCE_AGENT_HOST_PROFILE_FAMILY = {
unraid: 'Unraid',
} as const;
export const SOURCE_AGENT_HOST_PROFILE_GOVERNANCE_STATE = {
unraid: 'supported',
} as const;
export const SOURCE_AGENT_HOST_PROFILE_READINESS_STAGE = {
unraid: 'supported',
} as const;
export const SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS = {
unraid: ['unraid'],
} as const;
export const SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR = {
unraid: {
setup: 'supported',
visibility: 'supported',
workloads: 'n/a',
storage: 'supported',
recovery: 'n/a',
alerts: 'supported',
assistantRead: 'supported',
assistantControl: 'read-only',
},
} as const;
export const SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY = {
unraid: 'onprem',
} as const;
export const SOURCE_PLATFORM_FAMILY = {
agent: 'Pulse-managed host',
docker: 'Container runtime',
@@ -783,6 +833,17 @@ export type PlatformSupportFloor =
export type PlatformSupportFloorValue = PlatformSupportFloor[keyof PlatformSupportFloor];
export type GeneratedSourcePlatformOnboardingPath =
(typeof SOURCE_PLATFORM_ONBOARDING_PATH_KEYS)[number];
export type GeneratedAgentHostProfileId = (typeof AGENT_HOST_PROFILE_IDS)[number];
export type GeneratedAgentHostProfileManifestEntry =
(typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number];
export type AgentHostProfileGovernanceState =
(typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['governanceState'];
export type AgentHostProfileReadinessStage =
(typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['readinessStage'];
export type AgentHostProfileSupportFloor =
(typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['supportFloor'];
export type AgentHostProfileSupportFloorValue =
AgentHostProfileSupportFloor[keyof AgentHostProfileSupportFloor];
export type SourcePlatformStorageFamily =
(typeof SOURCE_PLATFORM_MANIFEST_ENTRIES)[number]['storageFamily'];
export type GeneratedPlatformType = (typeof PLATFORM_TYPE_KEYS)[number];
@@ -1,11 +1,19 @@
import {
ADMITTED_PLATFORM_IDS,
AGENT_HOST_PROFILE_IDS,
DEFAULT_INFRASTRUCTURE_SOURCE_ORDER,
KNOWN_SOURCE_PLATFORM_KEYS,
PLATFORM_TYPE_KEYS,
PLATFORM_SUPPORT_MANIFEST_SOURCE,
PRESENTATION_ONLY_PLATFORM_IDS,
PLATFORM_SUPPORT_MANIFEST,
SOURCE_AGENT_HOST_PROFILE_FAMILY,
SOURCE_AGENT_HOST_PROFILE_GOVERNANCE_STATE,
SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS,
SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES,
SOURCE_AGENT_HOST_PROFILE_READINESS_STAGE,
SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY,
SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR,
SOURCE_PLATFORM_CANONICAL_PROJECTIONS,
SOURCE_PLATFORM_ONBOARDING_PATH_KEYS,
SOURCE_PLATFORM_ONBOARDING_PATHS,
@@ -20,6 +28,12 @@ import {
SOURCE_PLATFORM_STORAGE_FAMILY,
SOURCE_PLATFORM_SUPPORT_FLOOR,
SUPPORTED_PLATFORM_IDS,
type AgentHostProfileGovernanceState,
type AgentHostProfileReadinessStage,
type AgentHostProfileSupportFloor,
type AgentHostProfileSupportFloorValue,
type GeneratedAgentHostProfileId,
type GeneratedAgentHostProfileManifestEntry,
type GeneratedKnownSourcePlatform,
type GeneratedSourcePlatformOnboardingPath,
type GeneratedSourcePlatformManifestEntry,
@@ -33,7 +47,13 @@ import {
} from '@/utils/platformSupportManifest.generated';
export type SourcePlatformManifestEntry = GeneratedSourcePlatformManifestEntry;
export type SourceAgentHostProfileManifestEntry = GeneratedAgentHostProfileManifestEntry;
export type {
AgentHostProfileGovernanceState,
AgentHostProfileReadinessStage,
AgentHostProfileSupportFloor,
AgentHostProfileSupportFloorValue,
GeneratedAgentHostProfileId as AgentHostProfileId,
GeneratedKnownSourcePlatform,
GeneratedSourcePlatformOnboardingPath as SourcePlatformOnboardingPath,
PlatformPrimaryMode,
@@ -48,16 +68,32 @@ export type {
const entriesById = new Map<string, SourcePlatformManifestEntry>(
SOURCE_PLATFORM_MANIFEST_ENTRIES.map((platform) => [platform.id, platform] as const),
);
const agentHostProfileEntriesById = new Map<string, SourceAgentHostProfileManifestEntry>(
SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES.map((profile) => [profile.id, profile] as const),
);
const agentHostProfileEntriesByToken = new Map<string, SourceAgentHostProfileManifestEntry>(
SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES.flatMap((profile) =>
profile.hostIdentityTokens.map((token) => [token, profile] as const),
),
);
const EMPTY_ONBOARDING_PATHS: readonly GeneratedSourcePlatformOnboardingPath[] = [];
export {
ADMITTED_PLATFORM_IDS,
AGENT_HOST_PROFILE_IDS,
DEFAULT_INFRASTRUCTURE_SOURCE_ORDER,
KNOWN_SOURCE_PLATFORM_KEYS,
PLATFORM_SUPPORT_MANIFEST_SOURCE,
PLATFORM_TYPE_KEYS,
PRESENTATION_ONLY_PLATFORM_IDS,
PLATFORM_SUPPORT_MANIFEST,
SOURCE_AGENT_HOST_PROFILE_FAMILY,
SOURCE_AGENT_HOST_PROFILE_GOVERNANCE_STATE,
SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS,
SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES,
SOURCE_AGENT_HOST_PROFILE_READINESS_STAGE,
SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY,
SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR,
SOURCE_PLATFORM_CANONICAL_PROJECTIONS,
SOURCE_PLATFORM_ONBOARDING_PATH_KEYS,
SOURCE_PLATFORM_ONBOARDING_PATHS,
@@ -84,6 +120,24 @@ export const getSourcePlatformManifestEntry = (
return entriesById.get(platformId) || null;
};
export const getAgentHostProfileManifestEntry = (
value: string | null | undefined,
): SourceAgentHostProfileManifestEntry | null => {
const normalized = (value || '').trim().toLowerCase();
if (!normalized) return null;
return (
agentHostProfileEntriesById.get(normalized) ||
agentHostProfileEntriesByToken.get(normalized) ||
null
);
};
export const getAgentHostProfileFamily = (value: string | null | undefined): string | null => {
const manifestProfile = getAgentHostProfileManifestEntry(value);
return manifestProfile?.family ?? null;
};
export const getSourcePlatformStorageFamily = (
value: string | null | undefined,
): SourcePlatformStorageFamily | null => {
+25 -1
View File
@@ -462,9 +462,11 @@ func connectionFleetRemoteControl(conn Connection) string {
}
func connectionAgentIdentityForHost(host models.Host) *ConnectionAgentIdentity {
hostProfile := connectionAgentHostProfileForHost(host)
identity := &ConnectionAgentIdentity{
Hostname: strings.TrimSpace(host.Hostname),
Platform: strings.TrimSpace(host.Platform),
Platform: connectionAgentPlatformForHost(host, hostProfile),
HostProfile: hostProfile,
OSName: strings.TrimSpace(host.OSName),
OSVersion: strings.TrimSpace(host.OSVersion),
KernelVersion: strings.TrimSpace(host.KernelVersion),
@@ -474,6 +476,7 @@ func connectionAgentIdentityForHost(host models.Host) *ConnectionAgentIdentity {
}
if identity.Hostname == "" &&
identity.Platform == "" &&
identity.HostProfile == "" &&
identity.OSName == "" &&
identity.OSVersion == "" &&
identity.KernelVersion == "" &&
@@ -485,6 +488,27 @@ func connectionAgentIdentityForHost(host models.Host) *ConnectionAgentIdentity {
return identity
}
func connectionAgentPlatformForHost(host models.Host, hostProfile string) string {
platform := strings.TrimSpace(host.Platform)
if hostProfile == "unraid" && (platform == "" || strings.EqualFold(platform, "unraid")) {
return "linux"
}
return platform
}
func connectionAgentHostProfileForHost(host models.Host) string {
if host.Unraid != nil {
return "unraid"
}
if strings.EqualFold(strings.TrimSpace(host.OSName), "unraid") {
return "unraid"
}
if strings.EqualFold(strings.TrimSpace(host.Platform), "unraid") {
return "unraid"
}
return ""
}
func connectionHostAliasesForAgent(host models.Host, name, address string) []string {
values := []string{name, address, host.Hostname, host.ReportIP}
for _, iface := range host.NetworkInterfaces {
+38 -1
View File
@@ -257,7 +257,8 @@ func TestBuildConnections_AgentHostAliasesIncludeReportedIdentityHints(t *testin
got[0].AgentIdentity,
&ConnectionAgentIdentity{
Hostname: "pi",
Platform: "unraid",
Platform: "linux",
HostProfile: "unraid",
OSName: "Unraid",
OSVersion: "7.1.0",
KernelVersion: "6.12.0",
@@ -270,6 +271,42 @@ func TestBuildConnections_AgentHostAliasesIncludeReportedIdentityHints(t *testin
}
}
func TestBuildConnections_AgentHostProfileFromUnraidStorageFacts(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
hosts: []models.Host{
{
ID: "tower",
Hostname: "tower",
Platform: "linux",
Unraid: &models.HostUnraidStorage{ArrayStarted: true},
LastSeen: now,
AgentVersion: "6.12.10",
CommandsEnabled: false,
},
},
now: now,
}
got := buildConnections(in)
if len(got) != 1 {
t.Fatalf("expected 1 connection, got %d", len(got))
}
identity := got[0].AgentIdentity
if identity == nil {
t.Fatal("expected agent identity metadata")
}
if identity.HostProfile != "unraid" {
t.Fatalf("host profile = %q, want %q", identity.HostProfile, "unraid")
}
if identity.Platform != "linux" {
t.Fatalf("platform = %q, want %q", identity.Platform, "linux")
}
if identity.OSName != "" {
t.Fatalf("os name = %q, want empty when source identity is absent", identity.OSName)
}
}
func TestBuildConnections_AgentVersionUpdateAvailability(t *testing.T) {
now := time.Now()
in := aggregatorInputs{
+1
View File
@@ -81,6 +81,7 @@ type ConnectionError struct {
type ConnectionAgentIdentity struct {
Hostname string `json:"hostname,omitempty"`
Platform string `json:"platform,omitempty"`
HostProfile string `json:"hostProfile,omitempty"`
OSName string `json:"osName,omitempty"`
OSVersion string `json:"osVersion,omitempty"`
KernelVersion string `json:"kernelVersion,omitempty"`
+3 -2
View File
@@ -12826,7 +12826,8 @@ func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) {
Source: ConnectionSourceAgent,
AgentIdentity: &ConnectionAgentIdentity{
Hostname: "host-1",
Platform: "unraid",
Platform: "linux",
HostProfile: "unraid",
OSName: "Unraid",
OSVersion: "7.1.0",
KernelVersion: "6.12.0",
@@ -12859,7 +12860,7 @@ func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) {
t.Fatalf("marshal agent Connection: %v", err)
}
want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.168.0.2"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-04-22T12:00:00Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"unraid","osName":"Unraid","osVersion":"7.1.0","kernelVersion":"6.12.0","architecture":"x86_64","reportIp":"192.168.0.2","commandsEnabled":true},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.2","agentUpdateAvailable":true,"fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"behind","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"update-available","remoteControl":"enabled"},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}`
want := `{"id":"agent:host-1","type":"agent","name":"host-1","address":"host-1","hostAliases":["host-1","192.168.0.2"],"state":"active","enabled":true,"surfaces":["host"],"scope":{"host":true},"lastSeen":"2026-04-22T12:00:00Z","source":"agent","agentIdentity":{"hostname":"host-1","platform":"linux","hostProfile":"unraid","osName":"Unraid","osVersion":"7.1.0","kernelVersion":"6.12.0","architecture":"x86_64","reportIp":"192.168.0.2","commandsEnabled":true},"agentVersion":"6.0.0","expectedAgentVersion":"6.0.2","agentUpdateAvailable":true,"fleet":{"enrollmentState":"enrolled","livenessState":"active","versionDrift":"behind","adapterHealth":"healthy","configRollout":"reported","credentialStatus":"verified","updateStatus":"update-available","remoteControl":"enabled"},"capabilities":{"supportsPause":false,"supportsScope":false,"supportsTest":false}}`
assertJSONSnapshot(t, body, want)
}
+2
View File
@@ -997,6 +997,8 @@ func normalisePlatform(platform string) string {
switch platform {
case "darwin":
return "macos"
case "unraid":
return "linux"
default:
return platform
}
+35
View File
@@ -478,6 +478,41 @@ Platform = QNAP
t.Fatalf("osVersion = %q, want %q", agent.osVersion, "5.2.0")
}
})
t.Run("unraid from version file", func(t *testing.T) {
mc := &mockCollector{
goos: "linux",
hostInfoFn: func(context.Context) (*gohost.InfoStat, error) {
return &gohost.InfoStat{
Hostname: "unraid",
HostID: "hid",
Platform: "linux",
PlatformFamily: "linux",
PlatformVersion: "",
KernelArch: runtime.GOARCH,
}, nil
},
readFileFn: func(name string) ([]byte, error) {
switch name {
case "/etc/unraid-version":
return []byte("Unraid OS 7.1.0\n"), nil
default:
return nil, os.ErrNotExist
}
},
}
agent, err := New(Config{APIToken: "token", LogLevel: zerolog.InfoLevel, Collector: mc})
if err != nil {
t.Fatalf("New: %v", err)
}
if agent.osName != "Unraid" {
t.Fatalf("osName = %q, want %q", agent.osName, "Unraid")
}
if agent.osVersion != "7.1.0" {
t.Fatalf("osVersion = %q, want %q", agent.osVersion, "7.1.0")
}
})
}
func TestNew_UsesCustomCABundleForHTTPTransport(t *testing.T) {
+5
View File
@@ -50,6 +50,11 @@ func TestNormalisePlatform(t *testing.T) {
platform: "freebsd",
expected: "freebsd",
},
{
name: "unraid reports linux platform",
platform: "unraid",
expected: "linux",
},
{
name: "empty string",
platform: "",
+45 -1
View File
@@ -1,6 +1,11 @@
package hostagent
import "strings"
import (
"regexp"
"strings"
)
var unraidVersionPattern = regexp.MustCompile(`\b\d+(?:\.\d+)+(?:[-+._][A-Za-z0-9]+)*\b|\b\d+\b`)
func resolveHostOSIdentity(collector SystemCollector, osName, osVersion string) (string, string) {
currentName := strings.TrimSpace(osName)
@@ -24,6 +29,13 @@ func resolveHostOSIdentity(collector SystemCollector, osName, osVersion string)
return name, strings.TrimSpace(version)
}
if name, version, ok := detectUnraidOSIdentity(collector); ok {
if version == "" {
version = currentVersion
}
return name, strings.TrimSpace(version)
}
return currentName, currentVersion
}
@@ -138,6 +150,38 @@ func detectQNAPOSIdentity(collector SystemCollector) (string, string, bool) {
return "", "", false
}
func detectUnraidOSIdentity(collector SystemCollector) (string, string, bool) {
data, err := collector.ReadFile(hostAgentUnraidVersionPath)
if err != nil {
if _, statErr := collector.Stat(hostAgentUnraidVersionPath); statErr != nil {
return "", "", false
}
return "Unraid", "", true
}
version := cleanUnraidVersion(string(data))
return "Unraid", version, true
}
func cleanUnraidVersion(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if match := unraidVersionPattern.FindString(line); match != "" {
return match
}
}
return ""
}
func parseAssignmentConfig(content string) map[string]string {
parsed := make(map[string]string)
+346 -6
View File
@@ -14,15 +14,27 @@ import (
)
var (
numberedPlatformListRE = regexp.MustCompile("^\\d+\\.\\s+`([^`]+)`")
currentSupportMatrixRE = regexp.MustCompile("^\\|\\s+`([^`]+)`\\s+\\|")
currentSupportSetupRowRE = regexp.MustCompile("^\\|\\s+`([^`]+)`\\s+\\|\\s+([^|]+?)\\s+\\|")
numberedPlatformListRE = regexp.MustCompile("^\\d+\\.\\s+`([^`]+)`")
currentSupportMatrixRE = regexp.MustCompile("^\\|\\s+`([^`]+)`\\s+\\|")
currentSupportSetupRowRE = regexp.MustCompile("^\\|\\s+`([^`]+)`\\s+\\|\\s+([^|]+?)\\s+\\|")
agentHostProfileTableRowRE = regexp.MustCompile("^\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|")
)
type platformSupportManifest struct {
SchemaVersion int `json:"schema_version"`
DefaultInfrastructureSourceOrder []string `json:"default_infrastructure_source_order"`
Platforms []platformSupportManifestEntry `json:"platforms"`
SchemaVersion int `json:"schema_version"`
DefaultInfrastructureSourceOrder []string `json:"default_infrastructure_source_order"`
AgentHostProfiles []agentHostProfileManifestEntry `json:"agent_host_profiles"`
Platforms []platformSupportManifestEntry `json:"platforms"`
}
type agentHostProfileManifestEntry struct {
ID string `json:"id"`
Family string `json:"family"`
GovernanceState string `json:"governance_state"`
ReadinessStage string `json:"readiness_stage"`
HostIdentityTokens []string `json:"host_identity_tokens"`
SupportFloor map[string]string `json:"support_floor"`
StorageFamily string `json:"storage_family"`
}
type platformSupportManifestEntry struct {
@@ -47,6 +59,7 @@ func TestPlatformSupportManifestMatchesSupportModel(t *testing.T) {
classified := parsePlatformListSection(t, model, "### First-class platforms")
admitted := parsePlatformListSection(t, model, "### Admitted platforms (not yet supported)")
presentationOnly := parsePlatformListSection(t, model, "### Presentation-only platform vocabulary")
agentHostProfiles := parseAgentHostProfileSection(t, model, "### Agent Host Profiles")
matrix := parseCurrentSupportMatrixPlatforms(t, model)
if diff := diffPlatformSets(classified, matrix); diff != "" {
@@ -71,6 +84,52 @@ func TestPlatformSupportManifestMatchesSupportModel(t *testing.T) {
); diff != "" {
t.Fatalf("supported onboarding-path manifest drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformSets(
sortedAgentHostProfileIDs(agentHostProfiles),
manifestAgentHostProfileIDsByState(t, manifest, "supported"),
); diff != "" {
t.Fatalf("agent host profile manifest drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformFieldMap(
agentHostProfileFieldMap(agentHostProfiles, func(profile agentHostProfileSectionEntry) []string {
return []string{profile.Family}
}),
manifestAgentHostProfileFamiliesByState(t, manifest, "supported"),
); diff != "" {
t.Fatalf("agent host profile family drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformFieldMap(
agentHostProfileFieldMap(agentHostProfiles, func(profile agentHostProfileSectionEntry) []string {
return []string{profile.GovernanceState}
}),
manifestAgentHostProfileGovernanceStatesByState(t, manifest, "supported"),
); diff != "" {
t.Fatalf("agent host profile governance drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformFieldMap(
agentHostProfileFieldMap(agentHostProfiles, func(profile agentHostProfileSectionEntry) []string {
return []string{profile.ReadinessStage}
}),
manifestAgentHostProfileReadinessStagesByState(t, manifest, "supported"),
); diff != "" {
t.Fatalf("agent host profile readiness drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformFieldMap(
agentHostProfileFieldMap(agentHostProfiles, func(profile agentHostProfileSectionEntry) []string {
return append([]string(nil), profile.HostIdentityTokens...)
}),
manifestAgentHostProfileHostIdentityTokensByState(t, manifest, "supported"),
); diff != "" {
t.Fatalf("agent host profile host-identity tokens drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformFieldMap(
agentHostProfileFieldMap(agentHostProfiles, func(profile agentHostProfileSectionEntry) []string {
return []string{profile.StorageFamily}
}),
manifestAgentHostProfileStorageFamiliesByState(t, manifest, "supported"),
); diff != "" {
t.Fatalf("agent host profile storage-family drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformSets(classified, manifest.DefaultInfrastructureSourceOrder); diff != "" {
t.Fatalf("default infrastructure source ordering drifted from the canonical supported platform set:\n%s", diff)
}
@@ -167,6 +226,57 @@ func TestVMwareFixturesRemainAdmittedButNotSupported(t *testing.T) {
}
}
func TestUnraidRemainsInPresentationOnlyVocabularyAndAgentHostProfiles(t *testing.T) {
model := loadPlatformSupportModel(t)
manifest := loadPlatformSupportManifest(t)
supported := manifestPlatformsByState(t, manifest, "supported")
admitted := manifestPlatformsByState(t, manifest, "admitted")
presentationOnly := manifestPlatformsByState(t, manifest, "presentation-only")
profiles := parseAgentHostProfileSection(t, model, "### Agent Host Profiles")
if containsPlatform(supported, "unraid") {
t.Fatal("unraid must not appear in the current supported platform set")
}
if containsPlatform(admitted, "unraid") {
t.Fatal("unraid must not appear in the admitted platform set")
}
if !containsPlatform(presentationOnly, "unraid") {
t.Fatal("expected unraid to remain presentation-only platform vocabulary")
}
if !containsPlatform(sortedAgentHostProfileIDs(profiles), "unraid") {
t.Fatal("expected unraid to remain a supported agent host profile")
}
profile := requireAgentHostProfileManifestEntry(t, manifest, "unraid")
if profile.GovernanceState != "supported" {
t.Fatalf("unraid governance state = %q, want supported", profile.GovernanceState)
}
if profile.ReadinessStage != "supported" {
t.Fatalf("unraid readiness stage = %q, want supported", profile.ReadinessStage)
}
if diff := diffPlatformSets([]string{"unraid"}, profile.HostIdentityTokens); diff != "" {
t.Fatalf("unraid host identity tokens drifted from the host-profile model:\n%s", diff)
}
if profile.StorageFamily != "onprem" {
t.Fatalf("unraid storage family = %q, want onprem", profile.StorageFamily)
}
if got := profile.SupportFloor["assistant_control"]; got != "read-only" {
t.Fatalf("unraid assistant control support floor = %q, want read-only", got)
}
if got := profile.SupportFloor["storage"]; got != "supported" {
t.Fatalf("unraid storage support floor = %q, want supported", got)
}
if !strings.Contains(model, "### Agent Host Profiles") {
t.Fatal("expected platform support model to keep an agent host profiles section")
}
if !strings.Contains(
model,
"| `unraid` | `Unraid` | `supported` | `supported` | `unraid` | `onprem` |",
) {
t.Fatal("expected platform support model to keep the unraid host-profile row")
}
}
func assertAgentMockCoverage(t *testing.T, graph FixtureGraph) {
t.Helper()
@@ -495,6 +605,236 @@ func manifestPlatformOnboardingPaths(manifest platformSupportManifest) map[strin
return rows
}
type agentHostProfileSectionEntry struct {
Family string
GovernanceState string
ReadinessStage string
HostIdentityTokens []string
StorageFamily string
}
func parseAgentHostProfileSection(
t *testing.T,
model string,
heading string,
) map[string]agentHostProfileSectionEntry {
t.Helper()
entries := make(map[string]agentHostProfileSectionEntry)
inSection := false
for _, raw := range strings.Split(model, "\n") {
line := strings.TrimSpace(raw)
switch {
case line == heading:
inSection = true
continue
case !inSection:
continue
case strings.HasPrefix(line, "### ") || strings.HasPrefix(line, "## "):
return requireNonEmptyAgentHostProfileMap(t, entries, heading)
case line == "" || strings.HasPrefix(line, "| ---"):
continue
}
matches := agentHostProfileTableRowRE.FindStringSubmatch(line)
if len(matches) != 7 {
continue
}
entries[matches[1]] = agentHostProfileSectionEntry{
Family: matches[2],
GovernanceState: matches[3],
ReadinessStage: matches[4],
HostIdentityTokens: parseInlineTokenList(matches[5]),
StorageFamily: matches[6],
}
}
return requireNonEmptyAgentHostProfileMap(t, entries, heading)
}
func parseInlineTokenList(value string) []string {
tokens := make([]string, 0, 1)
for _, token := range strings.Split(value, ",") {
token = strings.TrimSpace(token)
token = strings.Trim(token, "`")
if token == "" {
continue
}
tokens = append(tokens, token)
}
return tokens
}
func sortedAgentHostProfileIDs(profiles map[string]agentHostProfileSectionEntry) []string {
ids := make([]string, 0, len(profiles))
for id := range profiles {
ids = append(ids, id)
}
sort.Strings(ids)
return ids
}
func agentHostProfileFieldMap(
profiles map[string]agentHostProfileSectionEntry,
selector func(agentHostProfileSectionEntry) []string,
) map[string][]string {
rows := make(map[string][]string, len(profiles))
for id, profile := range profiles {
rows[id] = append([]string(nil), selector(profile)...)
}
return rows
}
func requireNonEmptyAgentHostProfileMap(
t *testing.T,
entries map[string]agentHostProfileSectionEntry,
heading string,
) map[string]agentHostProfileSectionEntry {
t.Helper()
if len(entries) == 0 {
t.Fatalf("expected %s to declare at least one agent host profile", heading)
}
return entries
}
func manifestAgentHostProfileIDsByState(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
) []string {
t.Helper()
ids := make([]string, 0)
for _, profile := range manifest.AgentHostProfiles {
if strings.TrimSpace(profile.ID) == "" {
t.Fatal("expected agent host profile ids to be non-empty")
}
if strings.TrimSpace(profile.GovernanceState) == "" {
t.Fatalf("expected agent host profile manifest to classify %s", profile.ID)
}
if profile.GovernanceState == governanceState {
ids = append(ids, profile.ID)
}
}
return requireNonEmptyPlatformList(
t,
ids,
fmt.Sprintf("manifest agent host profiles with state %s", governanceState),
)
}
func manifestAgentHostProfileFieldMap(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
selector func(agentHostProfileManifestEntry) []string,
label string,
) map[string][]string {
t.Helper()
rows := make(map[string][]string)
for _, profile := range manifest.AgentHostProfiles {
if profile.GovernanceState != governanceState {
continue
}
rows[profile.ID] = append([]string(nil), selector(profile)...)
}
return requireNonEmptyPlatformFieldMap(t, rows, label)
}
func manifestAgentHostProfileFamiliesByState(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
) map[string][]string {
return manifestAgentHostProfileFieldMap(
t,
manifest,
governanceState,
func(profile agentHostProfileManifestEntry) []string { return []string{profile.Family} },
fmt.Sprintf("manifest agent host profile families with state %s", governanceState),
)
}
func manifestAgentHostProfileGovernanceStatesByState(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
) map[string][]string {
return manifestAgentHostProfileFieldMap(
t,
manifest,
governanceState,
func(profile agentHostProfileManifestEntry) []string { return []string{profile.GovernanceState} },
fmt.Sprintf("manifest agent host profile governance with state %s", governanceState),
)
}
func manifestAgentHostProfileReadinessStagesByState(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
) map[string][]string {
return manifestAgentHostProfileFieldMap(
t,
manifest,
governanceState,
func(profile agentHostProfileManifestEntry) []string { return []string{profile.ReadinessStage} },
fmt.Sprintf("manifest agent host profile readiness with state %s", governanceState),
)
}
func manifestAgentHostProfileHostIdentityTokensByState(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
) map[string][]string {
return manifestAgentHostProfileFieldMap(
t,
manifest,
governanceState,
func(profile agentHostProfileManifestEntry) []string {
return append([]string(nil), profile.HostIdentityTokens...)
},
fmt.Sprintf("manifest agent host profile identity tokens with state %s", governanceState),
)
}
func manifestAgentHostProfileStorageFamiliesByState(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
) map[string][]string {
return manifestAgentHostProfileFieldMap(
t,
manifest,
governanceState,
func(profile agentHostProfileManifestEntry) []string { return []string{profile.StorageFamily} },
fmt.Sprintf("manifest agent host profile storage families with state %s", governanceState),
)
}
func requireAgentHostProfileManifestEntry(
t *testing.T,
manifest platformSupportManifest,
profileID string,
) agentHostProfileManifestEntry {
t.Helper()
for _, profile := range manifest.AgentHostProfiles {
if profile.ID == profileID {
return profile
}
}
t.Fatalf("expected agent host profile manifest entry for %s", profileID)
return agentHostProfileManifestEntry{}
}
func requireManifestPlatform(t *testing.T, manifest platformSupportManifest, platformID string) platformSupportManifestEntry {
t.Helper()
@@ -35,6 +35,8 @@ SUPPORT_FLOOR_FIELDS = (
"assistant_control",
)
VALID_SUPPORT_FLOOR_VALUES = {"supported", "augmentation-only", "read-only", "n/a"}
VALID_AGENT_HOST_PROFILE_GOVERNANCE_STATES = {"supported", "presentation-only"}
VALID_AGENT_HOST_PROFILE_READINESS_STAGES = {"supported", "presentation-only"}
def require_dict(value: Any, label: str) -> dict[str, Any]:
@@ -73,15 +75,125 @@ def unique_preserve_order(values: list[str]) -> list[str]:
return ordered
def normalize_support_floor(support_floor_record: Any, label: str) -> dict[str, str]:
record = require_dict(support_floor_record, label)
support_floor: dict[str, str] = {}
for field in SUPPORT_FLOOR_FIELDS:
value = require_string(record.get(field), f"{label}.{field}")
if value not in VALID_SUPPORT_FLOOR_VALUES:
raise ValueError(
f"expected {label}.{field} to be one of {sorted(VALID_SUPPORT_FLOOR_VALUES)}"
)
camel_field = field.split("_")[0] + "".join(part.title() for part in field.split("_")[1:])
support_floor[camel_field] = value
return support_floor
def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
schema_version = raw_manifest.get("schema_version")
if not isinstance(schema_version, int) or schema_version < 1:
raise ValueError("expected schema_version to be a positive integer")
raw_agent_host_profiles = raw_manifest.get("agent_host_profiles")
if not isinstance(raw_agent_host_profiles, list) or not raw_agent_host_profiles:
raise ValueError("expected agent_host_profiles to be a non-empty array")
raw_platforms = raw_manifest.get("platforms")
if not isinstance(raw_platforms, list) or not raw_platforms:
raise ValueError("expected platforms to be a non-empty array")
agent_host_profiles: list[dict[str, Any]] = []
agent_host_profile_ids: list[str] = []
for index, raw_profile in enumerate(raw_agent_host_profiles):
record = require_dict(raw_profile, f"agent_host_profiles[{index}]")
profile_id = require_lowercase_identifier(record.get("id"), f"agent_host_profiles[{index}].id")
if profile_id in agent_host_profile_ids:
raise ValueError(f"duplicate agent host profile id {profile_id}")
governance_state = require_string(
record.get("governance_state"), f"agent_host_profiles[{index}].governance_state"
)
if governance_state not in VALID_AGENT_HOST_PROFILE_GOVERNANCE_STATES:
raise ValueError(
"expected agent_host_profiles["
f"{index}].governance_state to be one of "
f"{sorted(VALID_AGENT_HOST_PROFILE_GOVERNANCE_STATES)}"
)
readiness_stage = require_string(
record.get("readiness_stage"), f"agent_host_profiles[{index}].readiness_stage"
)
if readiness_stage not in VALID_AGENT_HOST_PROFILE_READINESS_STAGES:
raise ValueError(
"expected agent_host_profiles["
f"{index}].readiness_stage to be one of "
f"{sorted(VALID_AGENT_HOST_PROFILE_READINESS_STAGES)}"
)
if governance_state == "supported" and readiness_stage != "supported":
raise ValueError(
f"supported agent host profile {profile_id} must use readiness_stage supported"
)
if governance_state == "presentation-only" and readiness_stage != "presentation-only":
raise ValueError(
"presentation-only agent host profile "
f"{profile_id} must use readiness_stage presentation-only"
)
host_identity_tokens = unique_preserve_order(
[
require_lowercase_identifier(
token,
f"agent_host_profiles[{index}].host_identity_tokens",
)
for token in require_string_list(
record.get("host_identity_tokens"),
f"agent_host_profiles[{index}].host_identity_tokens",
)
]
)
if governance_state == "supported" and not host_identity_tokens:
raise ValueError(
f"supported agent host profile {profile_id} must declare host identity tokens"
)
if governance_state == "presentation-only" and host_identity_tokens:
raise ValueError(
f"presentation-only agent host profile {profile_id} must not declare host identity tokens"
)
support_floor = normalize_support_floor(
record.get("support_floor"),
f"agent_host_profiles[{index}].support_floor",
)
if governance_state == "presentation-only" and any(
value != "n/a" for value in support_floor.values()
):
raise ValueError(
f"presentation-only agent host profile {profile_id} support floor must be n/a"
)
storage_family = require_string(
record.get("storage_family"), f"agent_host_profiles[{index}].storage_family"
)
if storage_family not in VALID_STORAGE_FAMILIES:
raise ValueError(
"expected agent_host_profiles["
f"{index}].storage_family to be one of "
f"{sorted(VALID_STORAGE_FAMILIES)}"
)
agent_host_profiles.append(
{
"id": profile_id,
"family": require_string(record.get("family"), f"agent_host_profiles[{index}].family"),
"governanceState": governance_state,
"readinessStage": readiness_stage,
"hostIdentityTokens": host_identity_tokens,
"supportFloor": support_floor,
"storageFamily": storage_family,
}
)
agent_host_profile_ids.append(profile_id)
platforms: list[dict[str, Any]] = []
known_ids: set[str] = set()
alias_map: dict[str, str] = {}
@@ -175,20 +287,10 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
if governance_state == "presentation-only" and canonical_projections:
raise ValueError(f"presentation-only platform {platform_id} must not declare projections")
support_floor_record = require_dict(record.get("support_floor"), f"platforms[{index}].support_floor")
support_floor: dict[str, str] = {}
for field in SUPPORT_FLOOR_FIELDS:
value = require_string(
support_floor_record.get(field),
f"platforms[{index}].support_floor.{field}",
)
if value not in VALID_SUPPORT_FLOOR_VALUES:
raise ValueError(
f"expected platforms[{index}].support_floor.{field} to be one of "
f"{sorted(VALID_SUPPORT_FLOOR_VALUES)}"
)
camel_field = field.split("_")[0] + "".join(part.title() for part in field.split("_")[1:])
support_floor[camel_field] = value
support_floor = normalize_support_floor(
record.get("support_floor"),
f"platforms[{index}].support_floor",
)
if governance_state == "presentation-only" and any(value != "n/a" for value in support_floor.values()):
raise ValueError(f"presentation-only platform {platform_id} support floor must be n/a")
@@ -274,6 +376,24 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
readiness_stage_by_id = {
platform["id"]: platform["readinessStage"] for platform in platforms
}
agent_host_profile_family_by_id = {
profile["id"]: profile["family"] for profile in agent_host_profiles
}
agent_host_profile_governance_state_by_id = {
profile["id"]: profile["governanceState"] for profile in agent_host_profiles
}
agent_host_profile_readiness_stage_by_id = {
profile["id"]: profile["readinessStage"] for profile in agent_host_profiles
}
agent_host_profile_host_identity_tokens_by_id = {
profile["id"]: profile["hostIdentityTokens"] for profile in agent_host_profiles
}
agent_host_profile_support_floor_by_id = {
profile["id"]: profile["supportFloor"] for profile in agent_host_profiles
}
agent_host_profile_storage_family_by_id = {
profile["id"]: profile["storageFamily"] for profile in agent_host_profiles
}
primary_mode_by_id = {
platform["id"]: platform["primaryMode"] for platform in platforms
}
@@ -297,6 +417,14 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
for platform in platforms
if platform["governanceState"] == "presentation-only"
]
first_class_profile_ids = sorted(
set(agent_host_profile_ids).intersection([*supported_ids, *admitted_ids])
)
if first_class_profile_ids:
raise ValueError(
"agent host profile ids must not also be supported or admitted platform ids: "
+ ", ".join(first_class_profile_ids)
)
platform_type_keys = [*supported_ids, *admitted_ids]
known_source_platform_keys = [*platform_type_keys, *presentation_only_ids, "generic"]
onboarding_path_keys = unique_preserve_order(
@@ -310,6 +438,8 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
return {
"schemaVersion": schema_version,
"defaultInfrastructureSourceOrder": default_order,
"agentHostProfiles": agent_host_profiles,
"agentHostProfileIds": agent_host_profile_ids,
"platforms": platforms,
"supportedPlatformIds": supported_ids,
"admittedPlatformIds": admitted_ids,
@@ -320,6 +450,12 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
"auditTokens": audit_tokens,
"displayTokens": display_tokens,
"onboardingPathKeys": onboarding_path_keys,
"agentHostProfileFamilyById": agent_host_profile_family_by_id,
"agentHostProfileGovernanceStateById": agent_host_profile_governance_state_by_id,
"agentHostProfileReadinessStageById": agent_host_profile_readiness_stage_by_id,
"agentHostProfileHostIdentityTokensById": agent_host_profile_host_identity_tokens_by_id,
"agentHostProfileSupportFloorById": agent_host_profile_support_floor_by_id,
"agentHostProfileStorageFamilyById": agent_host_profile_storage_family_by_id,
"familyById": family_by_id,
"readinessStageById": readiness_stage_by_id,
"primaryModeById": primary_mode_by_id,
@@ -354,10 +490,14 @@ def render_module(normalized: dict[str, Any], manifest_hash: str) -> str:
{
"schemaVersion": normalized["schemaVersion"],
"defaultInfrastructureSourceOrder": normalized["defaultInfrastructureSourceOrder"],
"agentHostProfiles": normalized["agentHostProfiles"],
"platforms": normalized["platforms"],
},
),
"export const SOURCE_PLATFORM_MANIFEST_ENTRIES = PLATFORM_SUPPORT_MANIFEST.platforms;\n",
"export const SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES =\n"
" PLATFORM_SUPPORT_MANIFEST.agentHostProfiles;\n",
render_const("AGENT_HOST_PROFILE_IDS", normalized["agentHostProfileIds"]),
render_const("SUPPORTED_PLATFORM_IDS", normalized["supportedPlatformIds"]),
render_const("ADMITTED_PLATFORM_IDS", normalized["admittedPlatformIds"]),
render_const("PRESENTATION_ONLY_PLATFORM_IDS", normalized["presentationOnlyPlatformIds"]),
@@ -371,6 +511,30 @@ def render_module(normalized: dict[str, Any], manifest_hash: str) -> str:
render_const("SOURCE_PLATFORM_AUDIT_TOKENS", normalized["auditTokens"]),
render_const("SOURCE_PLATFORM_DISPLAY_TOKENS", normalized["displayTokens"]),
render_const("SOURCE_PLATFORM_ONBOARDING_PATH_KEYS", normalized["onboardingPathKeys"]),
render_const(
"SOURCE_AGENT_HOST_PROFILE_FAMILY",
normalized["agentHostProfileFamilyById"],
),
render_const(
"SOURCE_AGENT_HOST_PROFILE_GOVERNANCE_STATE",
normalized["agentHostProfileGovernanceStateById"],
),
render_const(
"SOURCE_AGENT_HOST_PROFILE_READINESS_STAGE",
normalized["agentHostProfileReadinessStageById"],
),
render_const(
"SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS",
normalized["agentHostProfileHostIdentityTokensById"],
),
render_const(
"SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR",
normalized["agentHostProfileSupportFloorById"],
),
render_const(
"SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY",
normalized["agentHostProfileStorageFamilyById"],
),
render_const("SOURCE_PLATFORM_FAMILY", normalized["familyById"]),
render_const("SOURCE_PLATFORM_READINESS_STAGE", normalized["readinessStageById"]),
render_const("SOURCE_PLATFORM_PRIMARY_MODE", normalized["primaryModeById"]),
@@ -390,6 +554,17 @@ def render_module(normalized: dict[str, Any], manifest_hash: str) -> str:
"export type PlatformSupportFloorValue = PlatformSupportFloor[keyof PlatformSupportFloor];\n",
"export type GeneratedSourcePlatformOnboardingPath =\n"
" (typeof SOURCE_PLATFORM_ONBOARDING_PATH_KEYS)[number];\n",
"export type GeneratedAgentHostProfileId = (typeof AGENT_HOST_PROFILE_IDS)[number];\n",
"export type GeneratedAgentHostProfileManifestEntry =\n"
" (typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number];\n",
"export type AgentHostProfileGovernanceState =\n"
" (typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['governanceState'];\n",
"export type AgentHostProfileReadinessStage =\n"
" (typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['readinessStage'];\n",
"export type AgentHostProfileSupportFloor =\n"
" (typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['supportFloor'];\n",
"export type AgentHostProfileSupportFloorValue =\n"
" AgentHostProfileSupportFloor[keyof AgentHostProfileSupportFloor];\n",
"export type SourcePlatformStorageFamily =\n"
" (typeof SOURCE_PLATFORM_MANIFEST_ENTRIES)[number]['storageFamily'];\n",
"export type GeneratedPlatformType = (typeof PLATFORM_TYPE_KEYS)[number];\n",