From 669cce678716fe842f7c9ec3c9144d1196bfdbf9 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 31 Aug 2026 06:26:53 +0100 Subject: [PATCH] Restore main CI after guardrail and contract drift Three consecutive main runs were red because landed features outran their guardrails while change detection kept the affected shards out of their own runs. - Resync shipped copies of API.md, CODE_SIGNING_POLICY.md, and PRODUCTION_SECURITY.md from repo docs. - Track the schema v16 telemetry disclosure (workload-history adoption counts) in the settings architecture guardrail. - Track route-scoped TrueNAS hydration queries and the conditional Kind column class in the platform overview layout guardrails. - Track the contained command-authority copy (legacy combined command profile) in the installer and setup guide tests. - Match the sentence-split delivery retention copy in AlertDeliveryLogCard. - Provide selectedAgentUrl, insecureMode, and customCaPath in the InfrastructureWorkspace operations mock for the safe-collector commands. - Include the resource list facets block in the two API contract snapshots, normalizing the direct-marshal fixture the way every handler does. - Require the fail-closed installer checksum wording in the AGENT_SUBSTRATE doc contract. --- frontend-modern/public/docs/API.md | 1 + .../public/docs/CODE_SIGNING_POLICY.md | 36 +++++++++++++++---- .../public/docs/PRODUCTION_SECURITY.md | 12 +++++-- .../InfrastructureOperationsModel.test.tsx | 8 +++-- .../InfrastructureWorkspace.test.tsx | 3 ++ .../NodeModalSetupGuideSection.test.tsx | 4 ++- .../__tests__/settingsArchitecture.test.ts | 4 +-- .../alerts/AlertDeliveryLogCard.test.tsx | 2 +- .../platformOverviewLayout.guardrails.test.ts | 6 ++-- internal/api/contract_test.go | 11 +++--- 10 files changed, 66 insertions(+), 21 deletions(-) diff --git a/frontend-modern/public/docs/API.md b/frontend-modern/public/docs/API.md index 4f5e37e6d..f88740261 100644 --- a/frontend-modern/public/docs/API.md +++ b/frontend-modern/public/docs/API.md @@ -100,6 +100,7 @@ Returns the unified resource list with pagination + aggregations. Requires `moni Query params: - `type`: comma-separated list (e.g., `agent`, `vm`, `system-container`, `container`, `docker-service`, `storage`, `pbs`, `pmg`, `k8s-cluster`, `k8s-node`, `pod`, `k8s-deployment`, `physical_disk`, `ceph`) - `source`: comma-separated list (e.g., `proxmox`, `agent`, `docker`, `pbs`, `pmg`, `kubernetes`) +- `excludeSource`: comma-separated list of sources that must not be present on a matching resource - `status`: comma-separated list (`online`, `offline`, `warning`, `unknown`) - `parent`: parent resource ID - `cluster`: cluster name diff --git a/frontend-modern/public/docs/CODE_SIGNING_POLICY.md b/frontend-modern/public/docs/CODE_SIGNING_POLICY.md index cd83ce9e1..d4718aa89 100644 --- a/frontend-modern/public/docs/CODE_SIGNING_POLICY.md +++ b/frontend-modern/public/docs/CODE_SIGNING_POLICY.md @@ -75,19 +75,43 @@ Normal stable publication and stable dry runs select `signpath` directly. and verified independently after publication. - The GitHub-hosted `build-release-candidate.yml` assembly job emits SLSA v1 provenance for every candidate file after complete local validation. Its - portable Sigstore bundle is published as - `release-build-provenance.sigstore.json` and covered by the immutable - candidate manifest. Verification rejects self-hosted provenance. + Sigstore bundle is then added to the immutable candidate as + `release-build-provenance.sigstore.json`; publication cannot replace that + bundle without failing the candidate manifest. This preserves the exact + builder evidence for offline verification instead of recreating provenance + in the later publication job. +- The exact-version OCI Helm chart is published only by the hosted + `publish-helm-chart.yml` workflow. Its SHA-256 manifest digest and GitHub + build-provenance attestation must bind to the release source commit before + the digest enters `release-activation.json`. Activation recovery repeats + that verification, and Helm Pages refuses to advertise a chart whose OCI + tag, signer workflow, source commit, or digest has drifted from the immutable + activation packet. +- Release activation requires GitHub CLI 2.97.0 or newer, which includes the + literal signer-identity matcher fix. The shared + `scripts/require-safe-gh-attestation.sh` guard enforces this floor. The + published checksum manifest must carry build provenance from the exact + `build-release-candidate.yml` workflow and release source commit; + repository-level provenance is not sufficient. Immutable releases created + before portable candidate bundles remain verified against their original + `create-release.yml` publication provenance. Both paths reject provenance + emitted from a self-hosted runner. - Every new release is assembled and validated as a draft. Its activation marker is uploaded and digest-checked before publication; GitHub must then report the published release as immutable, protecting its tag and complete - asset set from replacement. + asset set from replacement. A GitHub-hosted publication preflight proves the + repository setting before compilation, signing, private staging, or draft + assembly begins, and activation repeats that setting check immediately before + publication to catch later drift. - Customer-facing image aliases, Helm indexes, paid-runtime pointers, and demo environments are not promoted until `gh release verify --repo rcourtman/Pulse` validates GitHub's signed release attestation and `gh release verify-asset --repo rcourtman/Pulse` binds the - downloaded activation marker to that attestation. Operators can use the same - commands to verify the packet and any downloaded release asset independently. + downloaded activation marker to that attestation. The activation verifier + also binds the release's downloaded `checksums.txt` to that immutable packet + and verifies its exact workflow and source provenance. Operators can use the + same commands to verify the packet and any downloaded release asset + independently. ## Project roles diff --git a/frontend-modern/public/docs/PRODUCTION_SECURITY.md b/frontend-modern/public/docs/PRODUCTION_SECURITY.md index 939b9fe98..6d2fc393f 100644 --- a/frontend-modern/public/docs/PRODUCTION_SECURITY.md +++ b/frontend-modern/public/docs/PRODUCTION_SECURITY.md @@ -36,9 +36,10 @@ temperature sensors, Docker or Podman sockets, host-local storage state, and some platform integrations require root or equivalent access. That is a real security boundary, not a cosmetic implementation detail. -The default posture limits that boundary: +The fresh-install posture limits that boundary: -- command execution is disabled unless an operator explicitly enables it; +- the service is marked `monitoring-only`, remote configuration cannot promote + it, and its credential omits `agent:exec`; - the health and Prometheus listener binds to `127.0.0.1:9191` by default; - generated systemd units apply service hardening including `NoNewPrivileges=true`, private temporary storage, and kernel/control-group @@ -48,6 +49,13 @@ The default posture limits that boundary: - Proxmox guest Docker inventory through `pct exec` is disabled by default and requires an explicit server setting. +The advanced **legacy combined command profile** is a separate trust decision. +It marks the root collector command-capable and gives its credential execution +scope so the same process can accept governed server requests. Existing +unmarked installations remain in a visible `legacy` compatibility state during +the migration. Agent Doctor reports both local authority and credential scope +so an over-scoped monitoring install is not mistaken for a safe default. + On standard Linux systemd hosts the installer also offers a supported least-privilege profile: `--least-privilege` runs the service as a dedicated `pulse-agent` system user, with optional `--grant-smart` and `--grant-pct` diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx index a598e639b..0f1926b21 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx @@ -254,7 +254,9 @@ describe('infrastructure operations model', () => { expect(infrastructureInstallerSectionSource).toContain( 'PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true', ); - expect(infrastructureInstallerSectionSource).toContain('bounded pct exec'); + expect(infrastructureInstallerSectionSource).toMatch( + /bounded[\s\S]{0,20}?pct exec<\/code>/, + ); expect(infrastructureInstallerSectionSource).toContain('Install on a Kubernetes node'); expect(infrastructureInstallerSectionSource).toContain( 'state.handleInstallProfileChange(presentation().preferredProfile)', @@ -288,10 +290,10 @@ describe('infrastructure operations model', () => { 'Generate an install token first. Pulse will then build copy-ready commands', ); expect(infrastructureInstallerSectionSource).toContain( - 'Allow Pulse-scoped command requests on this agent for Patrol actions and opted-in Proxmox LXC Docker inventory', + 'Install the transitional combined runtime that can accept server command requests', ); expect(infrastructureInstallerSectionSource).toContain( - 'Enable Pulse command execution (Patrol actions and Proxmox LXC Docker inventory)', + 'Enable legacy combined command profile', ); expect(infrastructureInstallerSectionSource).not.toContain('Patrol auto-fix'); expect(infrastructureInstallerSectionSource).not.toContain('auto-fix requires Pulse Pro'); diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index 84add8d14..768497ec8 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -92,6 +92,9 @@ vi.mock('../useInfrastructureOperationsState', () => ({ acknowledgeNoToken: vi.fn(), commandsUnlocked: () => true, confirmedNoToken: () => true, + customCaPath: () => '', + insecureMode: () => false, + selectedAgentUrl: () => 'http://pulse', getAgentConnectionUpgradeCommand: (connection: Connection, installFlags: string[] = []) => `upgrade ${connection.id}${installFlags.length > 0 ? ` ${installFlags.join(' ')}` : ''}`, getAgentConnectionUpgradeCommandRequiresToken: () => false, diff --git a/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx b/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx index 23c4fff25..232c6c8a8 100644 --- a/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx @@ -111,7 +111,9 @@ describe('NodeModalSetupGuideSection', () => { expect(screen.getAllByText(/Pulse Agent root service/i).length).toBeGreaterThan(0); expect(screen.getByText(/temperatures, SMART, ZFS, Ceph, and mdadm/i)).toBeInTheDocument(); expect(screen.getByText(/Docker inside Proxmox LXCs:/i)).toBeInTheDocument(); - expect(screen.getAllByText(/Pulse command execution/i).length).toBeGreaterThan(0); + expect(screen.getAllByText(/does not grant remote\s+command authority/i).length).toBeGreaterThan( + 0, + ); const tlsOverride = screen.getByRole('checkbox', { name: /Skip TLS certificate verification when downloading the installer/i, }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 496cc4973..c6928f191 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -890,13 +890,13 @@ describe('settings architecture guardrails', () => { 'coarse deployment and lifecycle buckets', ); expect(EN_MESSAGES['settings.general.telemetry.description']).toContain( - 'aggregate resource and outcome counts, coarse feature flags, and content-free Patrol, Assistant, and capability-API usage counters', + 'aggregate resource and outcome counts, coarse feature flags, content-free Patrol, Assistant, and capability-API usage counters, and four session-deduplicated workload-history adoption counts', ); expect(EN_MESSAGES['settings.general.telemetry.description']).not.toContain( 'Pulse Intelligence loop adoption', ); expect(EN_MESSAGES['settings.general.telemetry.description']).toContain( - 'identifiers, URLs, paths, locale, browser events, prompts, chat messages, command text, action output, token values, names, email addresses, or IP addresses', + 'identifiers, URLs, paths, locale, raw browser events, an event-level clickstream, prompts, chat messages, command text, action output, token values, names, email addresses, or IP addresses', ); expect(generalSettingsPanelSource).toContain('settings.general.telemetry.payloadAriaLabel'); expect(generalSettingsPanelSource).toContain('settings.general.telemetry.resetId'); diff --git a/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx b/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx index a464dc46d..c720fec13 100644 --- a/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx +++ b/frontend-modern/src/features/alerts/AlertDeliveryLogCard.test.tsx @@ -118,7 +118,7 @@ describe('AlertDeliveryLogCard', () => { expect(screen.getByText(/Completed attempts are retained for 7 days/)).toBeInTheDocument(); expect( - screen.getByText(/failures that exhausted retries remain available for 30 days/), + screen.getByText(/Failures that exhausted retries remain available for 30 days/), ).toBeInTheDocument(); const timestamps = Array.from(container.querySelectorAll('time')); expect(timestamps[0]).toHaveAttribute('datetime', log.entries[0].timestamp); diff --git a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts index 32803ba7b..2d6a7f9cf 100644 --- a/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts +++ b/frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts @@ -415,7 +415,7 @@ describe('platform overview layout guardrails', () => { it('keeps remaining platform phone identity and actions usable', () => { expect(truenasStorageTopologyTableSource).toMatch( - /sortKey="kind"[\s\S]{0,120}?class="platform-table-mobile-w-15 md:w-\[10%\]"/, + /sortKey="kind"[\s\S]{0,160}?platform-table-mobile-w-15 md:w-\[10%\]/, ); expect(truenasStorageTopologyTableSource).toContain("return 'pl-6 sm:pl-11'"); expect(truenasStorageTopologyTableSource).toMatch( @@ -491,7 +491,9 @@ describe('platform overview layout guardrails', () => { expect(truenasPageSurfaceSource).toContain('