Preserve resource URLs across runtime identity changes

Contract-Neutral: Docker URL migration touches shared monitor ingest files without changing the host deletion or re-enrollment contract.
This commit is contained in:
rcourtman
2026-07-23 22:20:47 +01:00
parent 0a95b25f41
commit 15dbabfe9a
43 changed files with 1790 additions and 134 deletions
@@ -4990,3 +4990,10 @@ restart. `internal/api/host_agent_removal_lifecycle_integration_test.go`,
`internal/monitoring/monitor_host_agent_removal_lifecycle_test.go`, and the
existing `scripts/installtests/agent_state_dir_lifecycle_test.go`,
`install_sh_test.go`, and `install_ps1_test.go` are the production-path proofs.
Config reload must also hand the replacement monitor's tenant-scoped guest,
Docker, and host metadata stores to shared API/config persistence consumers.
That handoff is lifecycle continuity only: it prevents post-reload agent
reports and resource projection from observing an orphaned metadata cache, and
does not add a report shape, credential, command, or remote-configuration
authority.
@@ -6719,3 +6719,9 @@ status/history read remains bounded and cannot trigger a replacement run.
`internal/api/ai_handlers_patrol_actions_additional_test.go`, and
`frontend-modern/src/features/patrol/__tests__/patrolRunAcceptance.test.ts`
are the focused acceptance, API, and cold-start timing proofs.
Assistant web-interface URL updates resolve metadata providers per organization
from the active tenant monitor. Provider replacement refreshes both the default
service and already-created tenant services after monitor reload, so no AI
service retains an orphaned cache or writes a tenant's URL through another
organization's guest, Docker, or host metadata store.
@@ -36,6 +36,7 @@ WWN only when the match is unique and the hardware identifier is usable.
1. `internal/api/contract_test.go`
1a. `internal/api/platform_connection_shared.go`
1b. `internal/api/metadata_handlers_shared.go`
1c. `internal/config/persistence_metadata_accessors.go`
2. `internal/api/resources.go`
3. `internal/api/discovery_handlers.go`
4. `internal/api/alerts.go`
@@ -1820,7 +1821,16 @@ a new API state machine, queue contract, or verification-accounting field.
be forgotten. `internal/api/metadata_handlers_shared.go` owns the
guest/docker metadata GET/PUT payload semantics (empty object instead of
null, zero record instead of 404 — pinned by
`TestContract_MetadataGetPayloadsUseZeroRecordsInsteadOf404`). Deploy
`TestContract_MetadataGetPayloadsUseZeroRecordsInsteadOf404`). Guest,
Docker, and host metadata handlers must resolve the active tenant monitor's
live metadata stores. `ConfigPersistence`, export/import, entitlement usage,
and reload-time router wiring must share those store
instances rather than retaining independent in-memory caches of the same
files; a successful API write must therefore be visible to backend resource
projection and configuration export immediately. Assistant URL updates must
resolve a provider per organization and refresh existing services after a
monitor replacement; a default provider must not be shared across tenants.
Deploy
preflights and jobs share `handleDeployJobStatus` /
`handleDeployJobEvents` in `internal/api/deploy_handlers.go`; recovery
points series/facets share `parseRecoveryListPointsOptions`; docker and
@@ -221,6 +221,11 @@ data cannot wrap into a fabricated healthy value.
58. `internal/monitoring/proxmox_action_observer.go`
59. `internal/monitoring/agent_fleet_doctor.go`
60. `internal/config/host_continuity.go`
61. `internal/monitoring/docker_metadata_migration.go`
62. `internal/monitoring/kubernetes_metadata_migration.go`
63. `internal/monitoring/metadata_stores.go`
63a. `internal/config/docker_metadata.go`
63b. `internal/config/guest_metadata.go`
## Shared Boundaries
@@ -1495,6 +1500,26 @@ Docker metadata and legacy app-container guest metadata to that key when a
Docker report is ingested, then prefer that stable guest key when projecting
unified app-container custom URLs. Runtime container IDs remain action and
metric identities, not the persistent URL metadata identity.
When a container retains its runtime ID but changes normalized name, monitoring
must move stable guest and Docker metadata to the new name and remove the
obsolete name key after a successful or already-resolved destination. Rename
migration must snapshot all sources before writing so swaps do not exchange
URLs accidentally, and ambiguous normalized source or target names must fail
closed. A later unrelated container that reuses the old name must not inherit
the renamed container's URL.
Kubernetes pod, Deployment, and Service web-interface metadata uses
`k8s-workload:<cluster>:<kind>:<namespace>:<name>` as its stable logical
identity. Monitoring must migrate a current legacy unified-resource key, plus
the legacy `k8s:<cluster>:pod:<pod-uid>` key for pods, when that resource is
observed. Runtime UIDs remain discovery and metrics coordinates. Every scope
component is required so a URL cannot cross cluster, namespace, or kind
boundaries, and an existing empty stable record is an intentional clear that
must block legacy fallback.
The monitor-owned guest, Docker, and host metadata stores are the live
in-memory authority for projection and migration. API, config export/import,
tenant usage, Assistant URL discovery, and reload paths must share those exact
tenant-scoped store instances rather than opening parallel caches over the
same files.
The same applies to proxmox topology coordinates exposed through typed views:
node, cluster, and instance accessors must return canonical trimmed values so
monitoring consumers do not fork topology grouping or labeling on `" pve-a "`
@@ -168,7 +168,8 @@ admission records a stable refusal without invoking executor or network code.
WebSocket startup, so a delayed or blocked first stream snapshot cannot
hide server-owned platform scopes; auth-mode branches must not fork
additional state probes or perform the same hydration twice.
17. `internal/api/slo.go` shared with `api-contracts`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary.
17. `frontend-modern/src/utils/workloads.ts` shared with `unified-resources`: the stable workload metadata identity helper is both a unified-resource persistence boundary and a workloads hot-path lookup boundary.
18. `internal/api/slo.go` shared with `api-contracts`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary.
Governed action decisions preserve SQLite and MemoryStore parity through one
shared pure append command. Every accepted approval advances a monotonic
@@ -1997,3 +1998,11 @@ The completed Operational Trust path preserves these explicit bounds:
covered by query-plan and retention tests;
6. Operational Trust telemetry uses bounded enumerated labels only and never
introduces resource-cardinality labels.
Workload URL persistence remains lookup-only on the hot path. Docker and
Kubernetes stable metadata candidates are derived synchronously from platform
identity already present on each workload; the workload hook may carry the
cluster scope needed for that calculation, but must not add per-row requests,
runtime-UID scans, or a second metadata cache. Kubernetes persistence uses the
fully scoped cluster, kind, namespace, and name identity; runtime pod UIDs are
legacy fallback coordinates only.
@@ -507,6 +507,14 @@
"unified-resources"
]
},
{
"path": "frontend-modern/src/utils/workloads.ts",
"rationale": "the stable workload metadata identity helper is both a unified-resource persistence boundary and a workloads hot-path lookup boundary",
"subsystems": [
"performance-and-scalability",
"unified-resources"
]
},
{
"path": "internal/agentcapabilities/action_target.go",
"rationale": "the Pulse Intelligence governed action target type and resource-to-action-target mapping vocabulary are both the Assistant approval/runtime routing contract and the canonical API/agent target contract for governed actions",
@@ -1880,6 +1888,7 @@
],
"exact_files": [
"internal/api/ai_handler_test.go",
"internal/api/ai_handlers_more_test.go",
"internal/api/ai_handlers_patrol_actions_additional_test.go",
"internal/api/ai_handlers_test.go",
"internal/api/ai_intelligence_handlers_test.go",
@@ -2462,6 +2471,7 @@
"frontend-modern/src/utils/apiTokenPresentation.ts",
"frontend-modern/src/utils/infrastructureSettingsPresentation.ts",
"internal/api/setup_script_render.go",
"internal/config/persistence_metadata_accessors.go",
"internal/websocket/hub.go",
"pkg/aicontracts/action_broker.go",
"pkg/aicontracts/fix_execution.go",
@@ -2807,6 +2817,20 @@
"internal/api/alerts_endpoints_test.go"
]
},
{
"id": "metadata-persistence-cache-authority",
"label": "metadata persistence live-store authority proof",
"match_prefixes": [],
"match_files": [
"internal/config/persistence_metadata_accessors.go"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"internal/api/metadata_handlers_test.go",
"internal/config/persistence_metadata_test.go"
]
},
{
"id": "backend-payload-contracts",
"label": "backend API payload proof",
@@ -2820,9 +2844,11 @@
],
"exact_files": [
"frontend-modern/src/types/api.ts",
"internal/api/ai_handlers_more_test.go",
"internal/api/ai_handlers_patrol_actions_additional_test.go",
"internal/api/contract_test.go",
"internal/api/host_agent_removal_lifecycle_integration_test.go",
"internal/api/metadata_handlers_test.go",
"internal/api/patrol_autopilot_test.go"
]
},
@@ -5132,6 +5158,8 @@
],
"owned_files": [
"docker-entrypoint.sh",
"internal/config/docker_metadata.go",
"internal/config/guest_metadata.go",
"internal/config/host_continuity.go",
"internal/dockeragent/collect.go",
"internal/dockeragent/docker_client.go",
@@ -5200,6 +5228,7 @@
"internal/config/host_continuity_test.go",
"internal/monitoring/issue1485_unraid_lifecycle_test.go",
"internal/monitoring/issue1595_collection_trust_test.go",
"internal/monitoring/monitor_docker_test.go",
"internal/monitoring/monitor_host_agent_removal_lifecycle_test.go",
"internal/monitoring/monitor_host_agents_test.go",
"internal/monitoring/monitor_package_updates_test.go",
@@ -5528,10 +5557,15 @@
"match_prefixes": [
"internal/monitoring/"
],
"match_files": [],
"match_files": [
"internal/config/docker_metadata.go",
"internal/config/guest_metadata.go"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"internal/config/docker_metadata_test.go",
"internal/config/guest_metadata_test.go",
"internal/monitoring/availability_poller_test.go",
"internal/monitoring/availability_udp_test.go",
"internal/monitoring/canonical_guardrails_test.go",
@@ -5540,6 +5574,7 @@
"internal/monitoring/monitor_alert_intent_test.go",
"internal/monitoring/monitor_alert_override_migration_test.go",
"internal/monitoring/monitor_backups_readstate_test.go",
"internal/monitoring/monitor_docker_test.go",
"internal/monitoring/monitor_host_agent_removal_lifecycle_test.go",
"internal/monitoring/monitor_host_agents_test.go",
"internal/unifiedresources/code_standards_test.go"
@@ -7013,6 +7048,7 @@
"frontend-modern/src/utils/serviceHealthPresentation.ts",
"frontend-modern/src/utils/sourcePlatforms.ts",
"frontend-modern/src/utils/sourceTypePresentation.ts",
"frontend-modern/src/utils/workloads.ts",
"frontend-modern/src/utils/workloadTypePresentation.ts",
"internal/api/resources.go",
"internal/platformsupport/manifest_generated.go"
@@ -7183,6 +7219,7 @@
"frontend-modern/src/routing/resourceLinks.ts",
"frontend-modern/src/types/resource.ts",
"frontend-modern/src/utils/resourceIdentity.ts",
"frontend-modern/src/utils/workloads.ts",
"internal/api/resources.go"
],
"allow_same_subsystem_tests": false,
@@ -7246,6 +7283,7 @@
"frontend-modern/src/stores/__tests__/websocket-unified.test.ts",
"frontend-modern/src/types/__tests__/resource.test.ts",
"frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts",
"frontend-modern/src/utils/__tests__/workloads.test.ts",
"internal/unifiedresources/code_standards_test.go"
]
},
@@ -1551,3 +1551,10 @@ accepts only a non-empty reason and bounded future expiry. Action offer and
planning require the `ai_autofix` entitlement in addition to current RBAC,
evidence, resource-capability, executor-readiness, approval, and action-policy
checks.
Guest, Docker, and host metadata reads, writes, and reloads must resolve the
active request tenant's monitor-owned store. A default-tenant cache must never
be reused to satisfy another organization, including Assistant-authored URL
updates, and stable workload identities must retain their full host or cluster,
namespace, kind, and name scope so a saved URL cannot cross a tenant or
resource boundary.
@@ -2278,6 +2278,11 @@ fields and nullable mode/kind metadata before presenting canonical item labels,
while storage detail drawers and filter controls must route summary series IDs,
source tones, and disk metrics through the shared storage helpers instead of
reconstructing them from local table state.
Config export/import remains adjacent persistence infrastructure rather than a
storage/recovery authority. Guest metadata import must reload the active
tenant monitor's live store so recovery and resource projections observe one
post-import state; it must not leave a parallel persistence cache or reload the
default tenant on behalf of another organization.
Host-agent removal tombstones in `internal/config/host_continuity.go` are an
adjacent agent-lifecycle security journal, not storage or recovery inventory.
@@ -128,6 +128,7 @@ life is derived.
58. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerServiceModel.ts`
59. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts`
60. `frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts`
60a. `frontend-modern/src/utils/workloads.ts`
61. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts`
62. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerHistoryState.ts`
63. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts`
@@ -470,6 +471,15 @@ the resource name through the shared `WebInterfaceNameLink` primitive. Proxmox
host table columns are governed by `proxmoxHostTableModel.ts`; that model must
not reintroduce a separate `Web` column or move the web-interface launch back
into a page-local icon cell.
Persisted workload URLs must also use the same stable identity in the workload
and infrastructure drawers. Docker app containers use
`app-container:<host>:name:<normalized-name>`; Kubernetes pods, Deployments,
and Services use
`k8s-workload:<cluster>:<kind>:<namespace>:<name>`. Runtime container IDs,
pod UIDs, and generated registry IDs remain fallback migration, action,
discovery, or metrics coordinates and must not be the primary metadata write
key. Missing cluster/host, namespace, kind, or name scope must fail closed to
the legacy resource key rather than constructing a broadened stable key.
Product-originated resource references may arrive as registered unified
resource IDs, source-specific IDs, or canonical identity aliases. The
unified-resource registry owns resolving those references through
@@ -630,7 +640,8 @@ container inventory table.
display/source family; `platformScopes` is the overlap set used when a
runtime workload belongs to both Docker and an owning infrastructure
platform.
18. `internal/api/resources.go` shared with `api-contracts`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary.
18. `frontend-modern/src/utils/workloads.ts` shared with `performance-and-scalability`: the stable workload metadata identity helper is both a unified-resource persistence boundary and a workloads hot-path lookup boundary.
19. `internal/api/resources.go` shared with `api-contracts`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary.
`/api/resources` type filters must accept URL-encoded comma-separated lists
from browser query builders exactly like literal comma separators, so Docker
/ Podman runtime pages do not lose `docker-host` inventory while requesting