Drive agent host profiles from platform manifest

(cherry picked from commit 4736358acc)
This commit is contained in:
rcourtman
2026-05-07 23:42:15 +01:00
parent 671431a862
commit b2fd7e22a9
23 changed files with 682 additions and 46 deletions
@@ -18,6 +18,7 @@
"host_identity_tokens": [
"unraid"
],
"runtime_platform": "linux",
"support_floor": {
"setup": "supported",
"visibility": "supported",
@@ -105,8 +105,11 @@ Agent host profiles must define:
3. governance state
4. readiness stage
5. host identity tokens
6. support-floor row
7. optional storage family
6. runtime platform fallback (the canonical agent runtime platform string used
when a profile-tagged host reports a non-canonical or empty runtime
platform — e.g. Unraid agents report `linux`)
7. support-floor row
8. optional storage family
## Canonical Ingestion Modes
@@ -255,9 +258,9 @@ 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` |
| Profile | Family | Governance | Readiness | Host identity tokens | Runtime platform | Storage family | Support floor |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `unraid` | `Unraid` | `supported` | `supported` | `unraid` | `linux` | `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
@@ -287,13 +290,16 @@ 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, 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.
classification (including the runtime-platform fallback) 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` and the
tracked backend projection in
`internal/platformsupport/manifest_generated.go` 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, host identity tokens, or runtime
platform fallbacks that are not declared in this document.
### Runtime variants
@@ -180,6 +180,10 @@ 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.
Agent runtime normalization must use that same governed host-profile manifest:
profile identity tokens and runtime platform fallback values such as
`unraid` -> `linux` are generated into the runtime resolver instead of being
redeclared in host-agent or settings-table branches.
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
@@ -804,6 +804,10 @@ the canonical monitored-system blocked payload.
`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.
The API aggregator must resolve host-profile identity tokens and runtime
platform fallback values through the generated platform-support backend
projection in `internal/platformsupport/manifest_generated.go`, so payload
normalization cannot drift into API-local Unraid or appliance branches.
`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
@@ -608,6 +608,11 @@ frontend primitive boundary.
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, 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.
The generated host-profile projection also carries runtime platform fallback
metadata for shared explanation and parity with backend normalization, but
frontend primitives must still render host-profile labels through
`agentIdentity.hostProfile` rather than prettifying presentation-only ids as
platform values.
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.
@@ -552,6 +552,11 @@ bypass the API fail-closed execution gate.
`docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json` through
`frontend-modern/src/utils/platformSupportManifest.ts`, not from
storage-local hard-coded provider arrays.
Agent host-profile appliance identity is adjacent context for storage
rows, not a storage-local platform classifier: Unraid storage may render
through the generated host-profile label and runtime platform fallback, but
storage/recovery must not reintroduce `unraid` as a first-class provider or
recovery platform.
32. Keep agentless availability endpoints neutral on the shared unified-resource and API contracts. When `internal/api/availability_handlers.go`, `internal/api/connections_handlers.go`, `internal/api/platform_mock_connections.go`, or `frontend-modern/src/hooks/useUnifiedResources.ts` surface `network-endpoint` availability resources, storage and recovery may consume their liveness as infrastructure context only; they must not reinterpret ping/TCP/HTTP endpoints as storage providers, backup targets, recovery repositories, or protected-workload evidence.
33. Keep infrastructure summary chart bucketing presentation-only on the adjacent shared API boundary. When `internal/api/router.go` normalizes mixed-cadence infrastructure history into equal-time summary buckets for operator-facing summary cards, storage and recovery may consume the resulting visual context only; they must not reinterpret those normalized chart samples as recovery freshness windows, backup cadence, or restore evidence.
34. Keep workload chart downsampling presentation-only on that same adjacent shared API boundary. When `internal/api/router.go` caps mixed-cadence workload history into equal-time buckets for operator-facing workload cards, storage and recovery may consume the resulting visual context only; they must not reinterpret those shaped chart samples as recovery freshness windows, backup cadence, or restore evidence.
@@ -616,6 +616,10 @@ 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.
Host-profile runtime fallback values are generated beside those labels so
agent-backed appliance reports can normalize to canonical runtime platforms
such as `linux` without making the appliance profile a unified-resource
platform.
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
@@ -69,12 +69,7 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
<div class="rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100">
<div class="font-medium">No supported API-backed platform detected at that address.</div>
<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>
}
>
<Show when={props.onChooseSourceTypeInstead}>
<button
type="button"
onClick={props.onChooseSourceTypeInstead}
@@ -82,9 +77,9 @@ export const AddressProbeStep: Component<AddressProbeStepProps> = (props) => {
>
Choose a source type instead
</button>
<span>, or if this is </span>
<span>. </span>
</Show>
this is one of the supported {getInfrastructureAgentHostProfileSupportText()},{' '}
If this is one of the supported {getInfrastructureAgentHostProfileSupportText()},{' '}
<Show
when={props.onInstallAgent}
fallback={<span class="font-medium">install Pulse Agent instead</span>}
@@ -86,10 +86,13 @@ describe('ConnectionEditor', () => {
fireEvent.click(screen.getByRole('button', { name: /probe address/i }));
await waitFor(() =>
expect(
screen.getByText(/No supported API-backed platform detected/i),
).toBeInTheDocument(),
expect(screen.getByText(/No supported API-backed platform detected/i)).toBeInTheDocument(),
);
const normalizedBodyText = document.body.textContent?.replace(/\s+/g, ' ') ?? '';
expect(normalizedBodyText).toContain(
'Choose a source type instead. If this is one of the supported Linux, macOS, Windows, FreeBSD, and Unraid host/appliance profiles',
);
expect(normalizedBodyText).not.toContain('if this is this is');
fireEvent.click(screen.getByRole('button', { name: /Choose a source type instead/i }));
expect(onBackToCatalog).toHaveBeenCalledTimes(1);
@@ -110,6 +113,12 @@ describe('ConnectionEditor', () => {
const agentButton = await screen.findByRole('button', {
name: /install pulse agent instead/i,
});
const normalizedBodyText = document.body.textContent?.replace(/\s+/g, ' ') ?? '';
expect(normalizedBodyText).toContain(
'If this is one of the supported Linux, macOS, Windows, FreeBSD, and Unraid host/appliance profiles',
);
expect(normalizedBodyText).not.toContain('catalog below, or if this is');
fireEvent.click(agentButton);
expect(screen.getByTestId('slot').textContent).toBe('slot:agent');
@@ -121,7 +130,12 @@ describe('ConnectionEditor', () => {
it('reopens the detect form from a selected credential slot', async () => {
mockedProbe.mockResolvedValueOnce({
candidates: [
{ type: 'truenas', host: 'https://truenas.lab', port: 443, hints: { product: 'TrueNAS SCALE' } },
{
type: 'truenas',
host: 'https://truenas.lab',
port: 443,
hints: { product: 'TrueNAS SCALE' },
},
],
probedMs: 142,
});
@@ -775,6 +775,8 @@ describe('InfrastructureWorkspace', () => {
expect(screen.getByText('Tower')).toBeInTheDocument();
expect(screen.getByText('Unraid 7.1.0')).toBeInTheDocument();
expect(screen.getByText('192.168.0.10')).toBeInTheDocument();
expect(towerAgent.agentIdentity?.hostProfile).toBe('unraid');
expect(towerAgent.agentIdentity?.platform).toBe('linux');
fireEvent.click(screen.getByRole('button', { name: /^Manage$/i }));
@@ -48,8 +48,6 @@ const prettifyPlatform = (platform?: string | null): string | null => {
return 'macOS';
case 'freebsd':
return 'FreeBSD';
case 'unraid':
return 'Unraid';
default:
return platform?.trim() ?? null;
}
@@ -13,6 +13,7 @@ import {
PLATFORM_TYPE_KEYS,
PRESENTATION_ONLY_PLATFORM_IDS,
getAgentHostProfileFamily,
getAgentHostProfileRuntimePlatform,
getSourcePlatformCanonicalProjections,
getSourcePlatformReadinessStage,
getSourcePlatformSupportFloor,
@@ -147,6 +148,7 @@ describe('sourcePlatforms', () => {
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(getAgentHostProfileRuntimePlatform('unraid')).toBe('linux');
expect(PLATFORM_TYPE_KEYS).not.toContain('unraid');
expect(PRESENTATION_ONLY_PLATFORM_IDS).toContain('unraid');
});
@@ -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: 48a9ef03ba53e39c676b8a3aaf3b608ffd4c2c0269c2e1e485170a006eadfe21
// Source SHA256: 769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3
export const PLATFORM_SUPPORT_MANIFEST_SOURCE = {
path: 'docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json',
sha256: '48a9ef03ba53e39c676b8a3aaf3b608ffd4c2c0269c2e1e485170a006eadfe21',
sha256: '769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3',
} as const;
export const PLATFORM_SUPPORT_MANIFEST = {
schemaVersion: 1,
@@ -25,6 +25,7 @@ export const PLATFORM_SUPPORT_MANIFEST = {
governanceState: 'supported',
readinessStage: 'supported',
hostIdentityTokens: ['unraid'],
runtimePlatform: 'linux',
supportFloor: {
setup: 'supported',
visibility: 'supported',
@@ -523,6 +524,9 @@ export const SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR = {
export const SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY = {
unraid: 'onprem',
} as const;
export const SOURCE_AGENT_HOST_PROFILE_RUNTIME_PLATFORM = {
unraid: 'linux',
} as const;
export const SOURCE_PLATFORM_FAMILY = {
agent: 'Pulse-managed host',
docker: 'Container runtime',
@@ -844,6 +848,8 @@ export type AgentHostProfileSupportFloor =
(typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['supportFloor'];
export type AgentHostProfileSupportFloorValue =
AgentHostProfileSupportFloor[keyof AgentHostProfileSupportFloor];
export type AgentHostProfileRuntimePlatform =
(typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['runtimePlatform'];
export type SourcePlatformStorageFamily =
(typeof SOURCE_PLATFORM_MANIFEST_ENTRIES)[number]['storageFamily'];
export type GeneratedPlatformType = (typeof PLATFORM_TYPE_KEYS)[number];
@@ -12,6 +12,7 @@ import {
SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS,
SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES,
SOURCE_AGENT_HOST_PROFILE_READINESS_STAGE,
SOURCE_AGENT_HOST_PROFILE_RUNTIME_PLATFORM,
SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY,
SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR,
SOURCE_PLATFORM_CANONICAL_PROJECTIONS,
@@ -30,6 +31,7 @@ import {
SUPPORTED_PLATFORM_IDS,
type AgentHostProfileGovernanceState,
type AgentHostProfileReadinessStage,
type AgentHostProfileRuntimePlatform,
type AgentHostProfileSupportFloor,
type AgentHostProfileSupportFloorValue,
type GeneratedAgentHostProfileId,
@@ -51,6 +53,7 @@ export type SourceAgentHostProfileManifestEntry = GeneratedAgentHostProfileManif
export type {
AgentHostProfileGovernanceState,
AgentHostProfileReadinessStage,
AgentHostProfileRuntimePlatform,
AgentHostProfileSupportFloor,
AgentHostProfileSupportFloorValue,
GeneratedAgentHostProfileId as AgentHostProfileId,
@@ -92,6 +95,7 @@ export {
SOURCE_AGENT_HOST_PROFILE_HOST_IDENTITY_TOKENS,
SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES,
SOURCE_AGENT_HOST_PROFILE_READINESS_STAGE,
SOURCE_AGENT_HOST_PROFILE_RUNTIME_PLATFORM,
SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY,
SOURCE_AGENT_HOST_PROFILE_SUPPORT_FLOOR,
SOURCE_PLATFORM_CANONICAL_PROJECTIONS,
@@ -138,6 +142,14 @@ export const getAgentHostProfileFamily = (value: string | null | undefined): str
return manifestProfile?.family ?? null;
};
export const getAgentHostProfileRuntimePlatform = (
value: string | null | undefined,
): AgentHostProfileRuntimePlatform | null => {
const manifestProfile = getAgentHostProfileManifestEntry(value);
if (!manifestProfile) return null;
return SOURCE_AGENT_HOST_PROFILE_RUNTIME_PLATFORM[manifestProfile.id];
};
export const getSourcePlatformStorageFamily = (
value: string | null | undefined,
): SourcePlatformStorageFamily | null => {
+11 -12
View File
@@ -10,6 +10,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/platformsupport"
pulseutils "github.com/rcourtman/pulse-go-rewrite/internal/utils"
)
@@ -489,24 +490,22 @@ func connectionAgentIdentityForHost(host models.Host) *ConnectionAgentIdentity {
}
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
return platformsupport.NormalizeRuntimePlatformForAgentHostProfile(hostProfile, host.Platform)
}
func connectionAgentHostProfileForHost(host models.Host) string {
if host.Unraid != nil {
return "unraid"
return connectionAgentHostProfileIDFromIdentity("unraid")
}
if strings.EqualFold(strings.TrimSpace(host.OSName), "unraid") {
return "unraid"
return connectionAgentHostProfileIDFromIdentity(host.OSName, host.Platform)
}
func connectionAgentHostProfileIDFromIdentity(values ...string) string {
profile, ok := platformsupport.AgentHostProfileForIdentity(values...)
if !ok {
return ""
}
if strings.EqualFold(strings.TrimSpace(host.Platform), "unraid") {
return "unraid"
}
return ""
return profile.ID
}
func connectionHostAliasesForAgent(host models.Host, name, address string) []string {
+3
View File
@@ -12859,6 +12859,9 @@ func TestContract_AgentConnectionPayloadIncludesVersionFields(t *testing.T) {
if err != nil {
t.Fatalf("marshal agent Connection: %v", err)
}
if conn.AgentIdentity.Platform == conn.AgentIdentity.HostProfile {
t.Fatalf("agent platform must remain runtime platform, got profile id %q", conn.AgentIdentity.Platform)
}
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)
+4 -2
View File
@@ -21,6 +21,7 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/agenttls"
"github.com/rcourtman/pulse-go-rewrite/internal/agentupdate"
"github.com/rcourtman/pulse-go-rewrite/internal/platformsupport"
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
"github.com/rcourtman/pulse-go-rewrite/internal/sensors"
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
@@ -997,9 +998,10 @@ func normalisePlatform(platform string) string {
switch platform {
case "darwin":
return "macos"
case "unraid":
return "linux"
default:
if runtimePlatform := platformsupport.RuntimePlatformForHostIdentityToken(platform); runtimePlatform != "" {
return runtimePlatform
}
return platform
}
}
+3
View File
@@ -512,6 +512,9 @@ Platform = QNAP
if agent.osVersion != "7.1.0" {
t.Fatalf("osVersion = %q, want %q", agent.osVersion, "7.1.0")
}
if got := normalisePlatform("unraid"); got != "linux" {
t.Fatalf("normalisePlatform(unraid) = %q, want linux", got)
}
})
}
@@ -11,13 +11,15 @@ import (
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/platformsupport"
)
var (
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+\\|")
agentHostProfileTableRowRE = regexp.MustCompile("^\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|\\s+`([^`]+)`\\s+\\|")
)
type platformSupportManifest struct {
@@ -33,6 +35,7 @@ type agentHostProfileManifestEntry struct {
GovernanceState string `json:"governance_state"`
ReadinessStage string `json:"readiness_stage"`
HostIdentityTokens []string `json:"host_identity_tokens"`
RuntimePlatform string `json:"runtime_platform"`
SupportFloor map[string]string `json:"support_floor"`
StorageFamily string `json:"storage_family"`
}
@@ -122,6 +125,14 @@ func TestPlatformSupportManifestMatchesSupportModel(t *testing.T) {
); 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.RuntimePlatform}
}),
manifestAgentHostProfileRuntimePlatformsByState(t, manifest, "supported"),
); diff != "" {
t.Fatalf("agent host profile runtime-platform fallback drifted from the canonical support model:\n%s", diff)
}
if diff := diffPlatformFieldMap(
agentHostProfileFieldMap(agentHostProfiles, func(profile agentHostProfileSectionEntry) []string {
return []string{profile.StorageFamily}
@@ -257,6 +268,9 @@ func TestUnraidRemainsInPresentationOnlyVocabularyAndAgentHostProfiles(t *testin
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.RuntimePlatform != "linux" {
t.Fatalf("unraid runtime platform = %q, want linux", profile.RuntimePlatform)
}
if profile.StorageFamily != "onprem" {
t.Fatalf("unraid storage family = %q, want onprem", profile.StorageFamily)
}
@@ -271,12 +285,56 @@ func TestUnraidRemainsInPresentationOnlyVocabularyAndAgentHostProfiles(t *testin
}
if !strings.Contains(
model,
"| `unraid` | `Unraid` | `supported` | `supported` | `unraid` | `onprem` |",
"| `unraid` | `Unraid` | `supported` | `supported` | `unraid` | `linux` | `onprem` |",
) {
t.Fatal("expected platform support model to keep the unraid host-profile row")
}
}
func TestPlatformSupportBackendProjectionMatchesSupportManifest(t *testing.T) {
manifest := loadPlatformSupportManifest(t)
profiles := platformsupport.AgentHostProfiles()
gotIDs := make([]string, 0, len(profiles))
gotTokens := make(map[string][]string, len(profiles))
gotRuntimePlatforms := make(map[string][]string, len(profiles))
for _, profile := range profiles {
if profile.GovernanceState != "supported" {
continue
}
gotIDs = append(gotIDs, profile.ID)
gotTokens[profile.ID] = append([]string(nil), profile.HostIdentityTokens...)
gotRuntimePlatforms[profile.ID] = []string{profile.RuntimePlatform}
}
if diff := diffPlatformSets(manifestAgentHostProfileIDsByState(t, manifest, "supported"), gotIDs); diff != "" {
t.Fatalf("backend agent host profile ids drifted from manifest:\n%s", diff)
}
if diff := diffPlatformFieldMap(
manifestAgentHostProfileHostIdentityTokensByState(t, manifest, "supported"),
gotTokens,
); diff != "" {
t.Fatalf("backend agent host profile identity tokens drifted from manifest:\n%s", diff)
}
if diff := diffPlatformFieldMap(
manifestAgentHostProfileRuntimePlatformsByState(t, manifest, "supported"),
gotRuntimePlatforms,
); diff != "" {
t.Fatalf("backend agent host profile runtime platforms drifted from manifest:\n%s", diff)
}
profile, ok := platformsupport.AgentHostProfileForIdentity("Unraid")
if !ok {
t.Fatal("expected backend projection to resolve Unraid identity token")
}
if profile.ID != "unraid" || profile.RuntimePlatform != "linux" {
t.Fatalf("backend Unraid profile = %+v, want id=unraid runtime=linux", profile)
}
if got := platformsupport.NormalizeRuntimePlatformForAgentHostProfile("unraid", "unraid"); got != "linux" {
t.Fatalf("backend runtime normalization = %q, want linux", got)
}
}
func assertAgentMockCoverage(t *testing.T, graph FixtureGraph) {
t.Helper()
@@ -610,6 +668,7 @@ type agentHostProfileSectionEntry struct {
GovernanceState string
ReadinessStage string
HostIdentityTokens []string
RuntimePlatform string
StorageFamily string
}
@@ -638,7 +697,7 @@ func parseAgentHostProfileSection(
}
matches := agentHostProfileTableRowRE.FindStringSubmatch(line)
if len(matches) != 7 {
if len(matches) != 8 {
continue
}
@@ -647,7 +706,8 @@ func parseAgentHostProfileSection(
GovernanceState: matches[3],
ReadinessStage: matches[4],
HostIdentityTokens: parseInlineTokenList(matches[5]),
StorageFamily: matches[6],
RuntimePlatform: matches[6],
StorageFamily: matches[7],
}
}
@@ -805,6 +865,20 @@ func manifestAgentHostProfileHostIdentityTokensByState(
)
}
func manifestAgentHostProfileRuntimePlatformsByState(
t *testing.T,
manifest platformSupportManifest,
governanceState string,
) map[string][]string {
return manifestAgentHostProfileFieldMap(
t,
manifest,
governanceState,
func(profile agentHostProfileManifestEntry) []string { return []string{profile.RuntimePlatform} },
fmt.Sprintf("manifest agent host profile runtime platforms with state %s", governanceState),
)
}
func manifestAgentHostProfileStorageFamiliesByState(
t *testing.T,
manifest platformSupportManifest,
@@ -0,0 +1,122 @@
// Code generated by scripts/release_control/generate_platform_support_backend_module.py.
// DO NOT EDIT.
// Source: docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json
// Source SHA256: 769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3
package platformsupport
import "strings"
// ManifestSourcePath is the repo-relative path to the canonical manifest used
// to produce this projection.
const ManifestSourcePath = "docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json"
// ManifestSourceSHA256 is the sha256 of the manifest bytes used to produce
// this projection.
const ManifestSourceSHA256 = "769261b515a0bd5baa4d17847a5d04b1f1a93ea0e5a8e0a048e85cd717def2e3"
// ManifestSchemaVersion is the schema_version field of the manifest.
const ManifestSchemaVersion = 1
// AgentHostProfileEntry is the generated projection of an agent host profile.
type AgentHostProfileEntry struct {
ID string
Family string
GovernanceState string
ReadinessStage string
HostIdentityTokens []string
RuntimePlatform string
}
// agentHostProfileEntries is the generated agent host profile manifest projection.
var agentHostProfileEntries = []AgentHostProfileEntry{
{
ID: "unraid",
Family: "Unraid",
GovernanceState: "supported",
ReadinessStage: "supported",
HostIdentityTokens: []string{"unraid"},
RuntimePlatform: "linux",
},
}
func AgentHostProfiles() []AgentHostProfileEntry {
out := make([]AgentHostProfileEntry, len(agentHostProfileEntries))
for index, profile := range agentHostProfileEntries {
out[index] = cloneAgentHostProfile(profile)
}
return out
}
func AgentHostProfileByID(value string) (AgentHostProfileEntry, bool) {
normalized := normalizeIdentityToken(value)
if normalized == "" {
return AgentHostProfileEntry{}, false
}
for _, profile := range agentHostProfileEntries {
if profile.ID == normalized {
return cloneAgentHostProfile(profile), true
}
}
return AgentHostProfileEntry{}, false
}
func AgentHostProfileForIdentity(values ...string) (AgentHostProfileEntry, bool) {
for _, value := range values {
for _, profile := range agentHostProfileEntries {
if agentHostProfileMatchesIdentity(profile, value) {
return cloneAgentHostProfile(profile), true
}
}
}
return AgentHostProfileEntry{}, false
}
func RuntimePlatformForHostIdentityToken(value string) string {
profile, ok := AgentHostProfileForIdentity(value)
if !ok {
return ""
}
return profile.RuntimePlatform
}
func NormalizeRuntimePlatformForAgentHostProfile(profileID, platform string) string {
reportedPlatform := strings.TrimSpace(platform)
profile, ok := AgentHostProfileByID(profileID)
if !ok || strings.TrimSpace(profile.RuntimePlatform) == "" {
return reportedPlatform
}
if reportedPlatform == "" || agentHostProfileMatchesIdentity(profile, reportedPlatform) {
return profile.RuntimePlatform
}
return reportedPlatform
}
func (profile AgentHostProfileEntry) MatchesIdentity(value string) bool {
return agentHostProfileMatchesIdentity(profile, value)
}
func agentHostProfileMatchesIdentity(profile AgentHostProfileEntry, value string) bool {
normalized := normalizeIdentityToken(value)
if normalized == "" {
return false
}
if profile.ID == normalized {
return true
}
for _, token := range profile.HostIdentityTokens {
if normalizeIdentityToken(token) == normalized {
return true
}
}
return false
}
func cloneAgentHostProfile(profile AgentHostProfileEntry) AgentHostProfileEntry {
profile.HostIdentityTokens = append([]string(nil), profile.HostIdentityTokens...)
return profile
}
func normalizeIdentityToken(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
@@ -0,0 +1,40 @@
package platformsupport
import "testing"
func TestAgentHostProfileResolverUsesManifestTokens(t *testing.T) {
profile, ok := AgentHostProfileForIdentity(" Unraid ")
if !ok {
t.Fatal("expected Unraid identity token to resolve")
}
if profile.ID != "unraid" {
t.Fatalf("profile id = %q, want unraid", profile.ID)
}
if profile.RuntimePlatform != "linux" {
t.Fatalf("runtime platform = %q, want linux", profile.RuntimePlatform)
}
profile.HostIdentityTokens[0] = "mutated"
profile, ok = AgentHostProfileForIdentity("unraid")
if !ok {
t.Fatal("expected resolver data to be immutable across callers")
}
if profile.HostIdentityTokens[0] != "unraid" {
t.Fatalf("identity tokens mutated globally: %+v", profile.HostIdentityTokens)
}
}
func TestNormalizeRuntimePlatformForAgentHostProfile(t *testing.T) {
if got := NormalizeRuntimePlatformForAgentHostProfile("unraid", ""); got != "linux" {
t.Fatalf("empty reported platform = %q, want linux", got)
}
if got := NormalizeRuntimePlatformForAgentHostProfile("unraid", "unraid"); got != "linux" {
t.Fatalf("legacy reported platform = %q, want linux", got)
}
if got := NormalizeRuntimePlatformForAgentHostProfile("unraid", "linux"); got != "linux" {
t.Fatalf("canonical reported platform = %q, want linux", got)
}
if got := NormalizeRuntimePlatformForAgentHostProfile("unknown", "unraid"); got != "unraid" {
t.Fatalf("unknown host profile platform = %q, want original value", got)
}
}
@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""Generate the typed backend platform-support projection from governance JSON."""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parents[2]
MANIFEST_PATH = REPO_ROOT / "docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json"
OUTPUT_PATH = REPO_ROOT / "internal/platformsupport/manifest_generated.go"
SOURCE_RELATIVE_PATH = "docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json"
VALID_AGENT_HOST_PROFILE_GOVERNANCE_STATES = {"supported", "presentation-only"}
VALID_AGENT_HOST_PROFILE_RUNTIME_PLATFORMS = {"linux", "macos", "windows", "freebsd"}
def require_dict(value: Any, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ValueError(f"expected {label} to be an object")
return value
def require_string(value: Any, label: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"expected {label} to be a non-empty string")
return value.strip()
def require_lowercase_identifier(value: Any, label: str) -> str:
normalized = require_string(value, label)
if normalized != normalized.lower():
raise ValueError(f"expected {label} to be lowercase")
return normalized
def require_string_list(value: Any, label: str) -> list[str]:
if not isinstance(value, list):
raise ValueError(f"expected {label} to be an array")
return [require_string(item, f"{label}[{index}]") for index, item in enumerate(value)]
def unique_preserve_order(values: list[str]) -> list[str]:
seen: set[str] = set()
ordered: list[str] = []
for value in values:
if value in seen:
continue
seen.add(value)
ordered.append(value)
return ordered
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")
agent_host_profiles: list[dict[str, Any]] = []
seen_ids: set[str] = set()
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 seen_ids:
raise ValueError(f"duplicate agent host profile id {profile_id}")
seen_ids.add(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)}"
)
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"
)
runtime_platform = require_lowercase_identifier(
record.get("runtime_platform"),
f"agent_host_profiles[{index}].runtime_platform",
)
if runtime_platform not in VALID_AGENT_HOST_PROFILE_RUNTIME_PLATFORMS:
raise ValueError(
"expected agent_host_profiles["
f"{index}].runtime_platform to be one of "
f"{sorted(VALID_AGENT_HOST_PROFILE_RUNTIME_PLATFORMS)}"
)
agent_host_profiles.append(
{
"id": profile_id,
"family": require_string(record.get("family"), f"agent_host_profiles[{index}].family"),
"governanceState": governance_state,
"readinessStage": require_string(
record.get("readiness_stage"),
f"agent_host_profiles[{index}].readiness_stage",
),
"hostIdentityTokens": host_identity_tokens,
"runtimePlatform": runtime_platform,
}
)
return {
"schemaVersion": schema_version,
"agentHostProfiles": agent_host_profiles,
}
def go_string(value: str) -> str:
return json.dumps(value, ensure_ascii=True)
def go_string_slice(values: list[str]) -> str:
if not values:
return "nil"
return "[]string{" + ", ".join(go_string(value) for value in values) + "}"
def render_module(normalized: dict[str, Any], manifest_hash: str) -> str:
profile_entries = []
for profile in normalized["agentHostProfiles"]:
profile_entries.append(
"\t{\n"
f"\t\tID: {go_string(profile['id'])},\n"
f"\t\tFamily: {go_string(profile['family'])},\n"
f"\t\tGovernanceState: {go_string(profile['governanceState'])},\n"
f"\t\tReadinessStage: {go_string(profile['readinessStage'])},\n"
f"\t\tHostIdentityTokens: {go_string_slice(profile['hostIdentityTokens'])},\n"
f"\t\tRuntimePlatform: {go_string(profile['runtimePlatform'])},\n"
"\t},\n"
)
return (
"// Code generated by scripts/release_control/generate_platform_support_backend_module.py.\n"
"// DO NOT EDIT.\n"
f"// Source: {SOURCE_RELATIVE_PATH}\n"
f"// Source SHA256: {manifest_hash}\n\n"
"package platformsupport\n\n"
'import "strings"\n\n'
"// ManifestSourcePath is the repo-relative path to the canonical manifest used\n"
"// to produce this projection.\n"
f"const ManifestSourcePath = {go_string(SOURCE_RELATIVE_PATH)}\n\n"
"// ManifestSourceSHA256 is the sha256 of the manifest bytes used to produce\n"
"// this projection.\n"
f"const ManifestSourceSHA256 = {go_string(manifest_hash)}\n\n"
"// ManifestSchemaVersion is the schema_version field of the manifest.\n"
f"const ManifestSchemaVersion = {normalized['schemaVersion']}\n\n"
"// AgentHostProfileEntry is the generated projection of an agent host profile.\n"
"type AgentHostProfileEntry struct {\n"
"\tID string\n"
"\tFamily string\n"
"\tGovernanceState string\n"
"\tReadinessStage string\n"
"\tHostIdentityTokens []string\n"
"\tRuntimePlatform string\n"
"}\n\n"
"// agentHostProfileEntries is the generated agent host profile manifest projection.\n"
"var agentHostProfileEntries = []AgentHostProfileEntry{\n"
+ "".join(profile_entries)
+ "}\n\n"
"func AgentHostProfiles() []AgentHostProfileEntry {\n"
"\tout := make([]AgentHostProfileEntry, len(agentHostProfileEntries))\n"
"\tfor index, profile := range agentHostProfileEntries {\n"
"\t\tout[index] = cloneAgentHostProfile(profile)\n"
"\t}\n"
"\treturn out\n"
"}\n\n"
"func AgentHostProfileByID(value string) (AgentHostProfileEntry, bool) {\n"
"\tnormalized := normalizeIdentityToken(value)\n"
"\tif normalized == \"\" {\n"
"\t\treturn AgentHostProfileEntry{}, false\n"
"\t}\n"
"\tfor _, profile := range agentHostProfileEntries {\n"
"\t\tif profile.ID == normalized {\n"
"\t\t\treturn cloneAgentHostProfile(profile), true\n"
"\t\t}\n"
"\t}\n"
"\treturn AgentHostProfileEntry{}, false\n"
"}\n\n"
"func AgentHostProfileForIdentity(values ...string) (AgentHostProfileEntry, bool) {\n"
"\tfor _, value := range values {\n"
"\t\tfor _, profile := range agentHostProfileEntries {\n"
"\t\t\tif agentHostProfileMatchesIdentity(profile, value) {\n"
"\t\t\t\treturn cloneAgentHostProfile(profile), true\n"
"\t\t\t}\n"
"\t\t}\n"
"\t}\n"
"\treturn AgentHostProfileEntry{}, false\n"
"}\n\n"
"func RuntimePlatformForHostIdentityToken(value string) string {\n"
"\tprofile, ok := AgentHostProfileForIdentity(value)\n"
"\tif !ok {\n"
"\t\treturn \"\"\n"
"\t}\n"
"\treturn profile.RuntimePlatform\n"
"}\n\n"
"func NormalizeRuntimePlatformForAgentHostProfile(profileID, platform string) string {\n"
"\treportedPlatform := strings.TrimSpace(platform)\n"
"\tprofile, ok := AgentHostProfileByID(profileID)\n"
"\tif !ok || strings.TrimSpace(profile.RuntimePlatform) == \"\" {\n"
"\t\treturn reportedPlatform\n"
"\t}\n"
"\tif reportedPlatform == \"\" || agentHostProfileMatchesIdentity(profile, reportedPlatform) {\n"
"\t\treturn profile.RuntimePlatform\n"
"\t}\n"
"\treturn reportedPlatform\n"
"}\n\n"
"func (profile AgentHostProfileEntry) MatchesIdentity(value string) bool {\n"
"\treturn agentHostProfileMatchesIdentity(profile, value)\n"
"}\n\n"
"func agentHostProfileMatchesIdentity(profile AgentHostProfileEntry, value string) bool {\n"
"\tnormalized := normalizeIdentityToken(value)\n"
"\tif normalized == \"\" {\n"
"\t\treturn false\n"
"\t}\n"
"\tif profile.ID == normalized {\n"
"\t\treturn true\n"
"\t}\n"
"\tfor _, token := range profile.HostIdentityTokens {\n"
"\t\tif normalizeIdentityToken(token) == normalized {\n"
"\t\t\treturn true\n"
"\t\t}\n"
"\t}\n"
"\treturn false\n"
"}\n\n"
"func cloneAgentHostProfile(profile AgentHostProfileEntry) AgentHostProfileEntry {\n"
"\tprofile.HostIdentityTokens = append([]string(nil), profile.HostIdentityTokens...)\n"
"\treturn profile\n"
"}\n\n"
"func normalizeIdentityToken(value string) string {\n"
"\treturn strings.ToLower(strings.TrimSpace(value))\n"
"}\n"
)
def format_go(source: str) -> str:
result = subprocess.run(
["gofmt"],
input=source,
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise RuntimeError(
"failed to format generated Go:\n"
f"{result.stderr.strip() or result.stdout.strip()}"
)
return result.stdout
def generate(check: bool) -> int:
manifest_bytes = MANIFEST_PATH.read_bytes()
manifest_hash = hashlib.sha256(manifest_bytes).hexdigest()
raw_manifest = require_dict(json.loads(manifest_bytes), "manifest root")
normalized = normalize_manifest(raw_manifest)
expected = format_go(render_module(normalized, manifest_hash))
if check:
current = OUTPUT_PATH.read_text(encoding="utf-8") if OUTPUT_PATH.exists() else ""
if current != expected:
print(
"platform support backend projection is stale; run "
"scripts/release_control/generate_platform_support_backend_module.py"
)
return 1
print("platform support backend projection is up to date")
return 0
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(expected, encoding="utf-8")
print(f"wrote {OUTPUT_PATH.relative_to(REPO_ROOT)}")
return 0
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true", help="fail if output would change")
return parser.parse_args()
if __name__ == "__main__":
raise SystemExit(generate(parse_args().check))
@@ -37,6 +37,7 @@ SUPPORT_FLOOR_FIELDS = (
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"}
VALID_AGENT_HOST_PROFILE_RUNTIME_PLATFORMS = {"linux", "macos", "windows", "freebsd"}
def require_dict(value: Any, label: str) -> dict[str, Any]:
@@ -160,6 +161,17 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
f"presentation-only agent host profile {profile_id} must not declare host identity tokens"
)
runtime_platform = require_lowercase_identifier(
record.get("runtime_platform"),
f"agent_host_profiles[{index}].runtime_platform",
)
if runtime_platform not in VALID_AGENT_HOST_PROFILE_RUNTIME_PLATFORMS:
raise ValueError(
"expected agent_host_profiles["
f"{index}].runtime_platform to be one of "
f"{sorted(VALID_AGENT_HOST_PROFILE_RUNTIME_PLATFORMS)}"
)
support_floor = normalize_support_floor(
record.get("support_floor"),
f"agent_host_profiles[{index}].support_floor",
@@ -188,6 +200,7 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
"governanceState": governance_state,
"readinessStage": readiness_stage,
"hostIdentityTokens": host_identity_tokens,
"runtimePlatform": runtime_platform,
"supportFloor": support_floor,
"storageFamily": storage_family,
}
@@ -394,6 +407,9 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
agent_host_profile_storage_family_by_id = {
profile["id"]: profile["storageFamily"] for profile in agent_host_profiles
}
agent_host_profile_runtime_platform_by_id = {
profile["id"]: profile["runtimePlatform"] for profile in agent_host_profiles
}
primary_mode_by_id = {
platform["id"]: platform["primaryMode"] for platform in platforms
}
@@ -456,6 +472,7 @@ def normalize_manifest(raw_manifest: dict[str, Any]) -> dict[str, Any]:
"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,
"agentHostProfileRuntimePlatformById": agent_host_profile_runtime_platform_by_id,
"familyById": family_by_id,
"readinessStageById": readiness_stage_by_id,
"primaryModeById": primary_mode_by_id,
@@ -535,6 +552,10 @@ def render_module(normalized: dict[str, Any], manifest_hash: str) -> str:
"SOURCE_AGENT_HOST_PROFILE_STORAGE_FAMILY",
normalized["agentHostProfileStorageFamilyById"],
),
render_const(
"SOURCE_AGENT_HOST_PROFILE_RUNTIME_PLATFORM",
normalized["agentHostProfileRuntimePlatformById"],
),
render_const("SOURCE_PLATFORM_FAMILY", normalized["familyById"]),
render_const("SOURCE_PLATFORM_READINESS_STAGE", normalized["readinessStageById"]),
render_const("SOURCE_PLATFORM_PRIMARY_MODE", normalized["primaryModeById"]),
@@ -565,6 +586,8 @@ def render_module(normalized: dict[str, Any], manifest_hash: str) -> str:
" (typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['supportFloor'];\n",
"export type AgentHostProfileSupportFloorValue =\n"
" AgentHostProfileSupportFloor[keyof AgentHostProfileSupportFloor];\n",
"export type AgentHostProfileRuntimePlatform =\n"
" (typeof SOURCE_AGENT_HOST_PROFILE_MANIFEST_ENTRIES)[number]['runtimePlatform'];\n",
"export type SourcePlatformStorageFamily =\n"
" (typeof SOURCE_PLATFORM_MANIFEST_ENTRIES)[number]['storageFamily'];\n",
"export type GeneratedPlatformType = (typeof PLATFORM_TYPE_KEYS)[number];\n",