Prevent the idempotent poll test from cancelling the first request after the handler starts but before the service records a successful synchronization.
Change-source: pulse-maintainer
Stripe checkouts could not be told apart by origin: every session was
stamped checkout_origin pulserelay_landing, so an upgrade started from an
RBAC gate looked identical to one started from the website. Gate CTAs now
stamp a closed-vocabulary source token (gate-<feature>, estate-card, or the
plans-page default) onto the owned billing plan route; the plan page threads
it into the purchase-start handoff, and the license server persists it on the
checkout intent and stamps Stripe metadata checkout_source.
Attribution is authenticated-session-only by construction. The public
/pricing route and the public pricing URL never carry it, and
getSelfHostedPurchaseStartUrl scrubs the parameter from forwarded query
strings so a crafted website link cannot claim in-app origin. Server-side,
source is validated against the same kebab vocabulary, skip-listed from the
Pulse Account portal redirect exactly as feature already is, and travels
only inside the handoff body. It is request-scoped and persists nothing on
the install; the cancel return echoes it so a retry keeps its origin.
The handoff field is omitempty because the license server decodes strictly,
so source-less installs stay compatible with a server either side of the
field being added. That ordering is recorded in pulse-pro OPERATIONS.md.
Two mandatory round-trips stood between an interested MSP and their first
screen, and neither was technical.
setup.sh required four image digests shipped as literal <pin>
placeholders, so the only way to get them was to ask. All four images are
publicly readable, so there was never anything to hand out. setup.sh now
resolves each blank pin to an immutable digest from its published tag via
buildx imagetools and writes it back to .env; hand-set values are left
alone.
setup.sh then died outright without a licence file, so nobody could start
the stack, create a workspace, or see the portal until a human minted a
licence for them. The control plane already ran unlicensed via
ProviderMSPPlanSourceEnvFallback; only the installer refused. A licence
path that is set but missing is still a hard failure, since that is a
misconfiguration rather than a choice.
Unlicensed now means evaluation rather than the cheapest paid tier. The
env fallback defaulted to msp_starter, handing every unlicensed
deployment the full 5-client Starter allowance and leaving no boundary
between evaluating and buying. Adds msp_eval at 2 workspaces: same
capabilities, smaller cap, not purchasable, not on the public ladder.
An isolation guarantee is the one claim a provider cannot evaluate from a
screenshot, and both MSP leads this year went quiet at exactly this step.
Contracts: cloud-paid records the unlicensed plan rule and the
strictly-below-paid invariant; deployment-installability records
credential-free, correspondence-free installability.
Verification: TestMSPEvalCapStaysBelowCheapestPaidTier,
TestCanonicalizePlanVersion_MSPEval,
TestProviderMSPSetupScriptSupportsUnlicensedEvaluation. The last was
negative-tested by reintroducing a <pin> placeholder and confirming it
fails. ensure_image_pins exercised against the live registries.
licensing, cloudcp, control-plane and installtests all green.
Transport-level failures from httpClient.Do (connection refused/reset,
DNS, TLS, timeouts) were wrapped as plain errors, so they fell through
userFriendlyActivationError's LicenseServerError branch to the generic
contact-support fallback instead of the retryable temporarily-unavailable
message. Users behind flaky networks got a dead-end message, and the
2026-08-01 rc.6 paid-runtime gate failure was misdiagnosed the same way.
Classify Do failures at the client layer as a retryable LicenseServerError
(code license_server_unreachable, StatusCode 0 so revocation/suspension/
migration status gates never match) with the cause reachable via Unwrap so
the status poller's context.Canceled check keeps working. The cloud-paid
transport boundary contract now names this classification. Tests pin the
classification, the cancellation chain, and the end-to-end user-facing
retryable message.
Add external_probe to the licensing feature enumeration, Pro tier
membership, self-hosted feature catalog, feature map, upgrade matrix
and pricing handoff, with the entitlement contract goldens updated.
The capability is served by the community binary - the entitlement
alone gates it, like relay. No existing free functionality changes:
local availability checks stay ungated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New *_branchcov0718_test.go files raise coverage of previously-uncovered pure
functions across ten packages. Covered areas include securityutil SSRF and URL
validation, truenas path and telemetry parse helpers, storagehealth SMART and
physical-disk risk assessment, vmware inventory sort keys and error classifiers,
servicediscovery token filtering and readiness, telemetry evidence-from-history,
models ToFrontend converters and frontend NormalizeCollections normalizers,
actionplanner type predicates and canonical resource-id sort, config API-token
accessors, and licensing state accessors.
Test-only, with no runtime or subsystem-contract change. Verified in a clean
worktree at HEAD with go vet and package tests green, gofmt clean, and every
named target function moved from 0 percent to covered.
Adds table-driven branch-coverage unit tests for previously uncovered pure
functions across internal/alerts/config, internal/ai/modelresolution,
internal/monitoring, internal/ai, internal/ai/qualification and pkg/licensing.
New test files only, with no source changes.
Covers per-subsystem alert-default normalization, configured model and provider
resolution, fleet-doctor identity and cluster-endpoint helpers, docker-state and
infrastructure-key mapping, investigation and SMART issue helpers, JSON and
autonomy normalization, and licensing feature-tier resolution. 65 TestBranchCov
functions in 7 files, all vet and gofmt clean.
The public pricing surface and in-app plan-selling copy now say
"Patrol investigates issues and explains the root cause" and
"Patrol applies safe fixes and verifies the result". Rename the
licensing catalog DisplayName/ComparisonName pairs to the title-cased
forms, align the ai_autofix UpgradeReason verbs (apply/verify the
result), regenerate the frontend catalog, and update the Go, vitest,
and Playwright pins plus README and PULSE_PRO docs.
Dead-code audit batch 1: five functions with zero references from main,
tests, or pulse-enterprise (deadcode -test, grep-verified per symbol,
enterprise compile-checked). Also renames the tlsutil test that
exercised the deleted compat alias's target to name the Unverified
function it actually calls.
Skipped from the audit list after re-verification: IsNilAlertPayload
(live pulse-enterprise consumer in internal/aialertanalysis).
GrantClaims and Claims gain a MaxUsers seat limit mirroring MaxGuests:
the grant JWT claim is copied at the activation bridge and surfaced
through EffectiveLimits()["max_users"], which the already-shipped
user-limit enforcement (MaxUsersLimitFromLicense) reads unchanged.
Inert by default: no plan sets max_users yet, so absent claim = 0 =
unlimited for every existing license and grant. The self-hosted
commercial volume scrub strips only max_monitored_systems and
max_guests; tests now pin that max_users survives it from both the
named field and explicit limits, plus the end-to-end proof that a grant
carrying max_users=3 enforces 3 and a grant without it stays unlimited.
Contract shape recorded in cloud-paid Extension Point 26; the grant
wire contract list gains max_users against the relay-server reference
(pulse-pro 95a18bd).
TierBusiness: Pro's feature set, 365-day history retention, uncapped
core monitoring like every self-hosted tier, Business display name, and
a slot in the min-tier ordering. Differentiation is by max_users limit,
retention, and support rather than features (multi-user is already a Pro
capability via RBAC; see specs/pricing-segmentation.md binder findings).
Dormant until the license server issues business plan versions; no
checkout, pricing, or UI surface references it yet. Contract shape
recorded as cloud-paid Extension Point 26 and pinned by
TestBusinessTierContractShape.
Manifest-backed MCP tools, prompts, and resources with surface affordance contracts; agent capability manifest and governance projection; API contract tests and capability route projection; operations-loop and intelligence-funnel telemetry; release-control subsystem documentation, registry, and tooling; licensing and configuration.
Two silent-downgrade paths in the v5→v6 migration are now visible and
self-healing:
License load/decrypt failure (was: one log line, no UI state): when
license.enc exists but cannot be read, getTenantComponents now persists
a terminal commercial_migration state with reason
persisted_license_unreadable, so the licence panel and global banner
tell the customer to re-enter their v5 key instead of leaving them to
discover missing Pro features.
Failed startup exchange (was: once per process, Community until manual
restart): exchange failures classified as pending — license-server
blips, DNS failures, rate limits — now schedule a background retry loop
with backoff (30s → 30m cap) that re-attempts the exchange until it
succeeds, hits a terminal classification, or the org's service activates
through another path. The loop stops cleanly on manual activation and
StopAllBackgroundLoops.
Registers the new files in the subsystem registry (cloud-paid
commercial-migration verification policy + shared ownership) and pins
both behaviors in the cloud-paid and api-contracts contracts.
v5.0.0 derived the license.enc AES key as sha256("pulse-license-" +
material) where material was the raw /etc/machine-id file content —
trailing newline included — with hostname and a fixed dev fallback as
alternates. v6 trims machine-id before hashing and dropped both
fallbacks, so licenses activated on v5.0.0 (before .license-key shipped
in v5.0.1) could never be decrypted and the December 2025 lifetime
cohort silently booted as Community.
Collect the raw v5 key materials at construction and try them verbatim
as legacy sha256 candidates during decrypt, alongside the existing
trimmed HKDF/legacy derivations. The write path is unchanged: persistent
random key + HKDF only. Pins the cloud-paid persistence contract to the
full v5.0.x compatibility-loader material set.
Go half of the licensing-policy client work from the v6 upgrade sweep
(license findings 5 and 6); the copy and docs landed in 3a51429a5 and
the license-server side lands in pulse-pro.
The grant refresh loop treated every HTTP 401 as revocation and wiped
the licence and activation file immediately, but the server returns 401
for token-not-found and token-expired as well as revocation, so a
single spurious 401 dropped a paying customer to Community until a
successful re-exchange. Activation state is now cleared only when the
401 carries an explicit revocation code (TOKEN_REVOKED,
INSTALLATION_REVOKED, LICENSE_REVOKED); any other 401 keeps the licence
and retries, letting the grant's 72h expiry plus the 7-day grace window
govern degradation. The bump_license_version immediate-refresh path in
the revocation poller had the same any-401 wipe and gets the same
guard.
ClassifyLegacyExchangeError now maps a 409 MAX_INSTALLATIONS exchange
error to failed/exchange_installation_limit with the
free_installation_slot action instead of the retryable
pending/exchange_conflict state, since retrying can never succeed until
a slot is freed server-side. Other 409s keep the pending conflict
semantics.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
In dev/demo builds HasFeature returned the implicit dev grant verdict
unconditionally, so features excluded from that grant (white_label,
multi_user, unlimited, env-gated multi_tenant) were denied even when an
explicitly activated, signature-verified license carried them - while
/api/license/status reported them active from the same claims. That
inconsistency made branded report rendering impossible to exercise in
dev builds. Excluded features now fall through to real entitlement
evaluation, so an activated license behaves the same in dev as in
release builds; the no-license dev posture is unchanged.
Provider-hosted MSP client workspaces previously sat at Community tier
forever: the runtime refreshed leases against the built-in Pulse Cloud URL
(hibernated, 522) instead of the provider control plane, and release-build
images verify leases only against the embedded Pulse key, which an
operator-generated CP_TRIAL_ACTIVATION_PRIVATE_KEY can never satisfy.
- Inject PULSE_PRO_TRIAL_SIGNUP_URL=CP_BASE_URL into client containers so
lease refresh targets the provider control plane.
- Chain trust through the Pulse-signed provider MSP license: the license
binds the provider's lease signing public key
(entitlement_signing_public_key claim); the control plane embeds the
license in every lease (provider_license claim); release-build runtimes
verify embedded Pulse root -> provider license -> lease signature.
- Cap chain-verified leases at ProviderChainedLeaseCapabilities: MSP tier
plus white_label (branded per-client reports), minus Pulse-service-backed
relay/mobile_app/push_notifications, which otherwise loop doomed
registrations against Pulse's relay.
- Fail fast at control-plane startup when the license does not bind the
configured signing key, instead of provisioning silently unlicensed
client workspaces.
Verified live on a Colima harness: release-tagged tenant image with test
embedded root, Traefik TLS, full provider-msp proof, tenant reports
valid=true plan_version=msp_growth with white_label and zero relay
failures.
Clears the dupl pairs outside internal/api:
- internal/alerts/pmg.go: the total/deferred/hold per-node queue checks
share evaluatePMGNodeQueueAlert; the historical short-circuit (a
below-threshold clear or invalid spec skips the node's remaining
checks) is preserved via the helper's skip-node return.
- internal/vmware/provider.go: the cloneInventory* family rides generic
cloneSliceWith / cloneShallowSlice helpers instead of fourteen copies
of the same nil/make/loop scaffold.
- internal/vmware/client.go + client_signals.go: the byte-identical
Automation and VI/JSON fetchers delegate to one getSessionScopedJSON.
- internal/vmware/fixtures.go: added to the dupl exclude list in
.golangci.yml — literal mock fixture catalogs are the same category as
the existing internal/mock/ exclusion.
- pkg/licensing/license_server_client.go: Activate and
ExchangeLegacyLicense share postActivationRequest (idempotent POST +
shared activation response decode).
- pkg/proxmox/client.go: LXC/VM RRD fetches share getGuestRRDData.
- pkg/pulsecli/actions.go is intentionally left for the api-contracts
slice.
Full test suites pass for internal/alerts, internal/vmware,
pkg/licensing, pkg/proxmox.
Dead-code sweep. Functions flagged unreachable by golang.org/x/tools/cmd/deadcode
and confirmed unused across pulse, pulse-enterprise, pulse-pro and pulse-mobile by
adversarial cross-repo verification. Cross-module reachability was checked
explicitly (only pkg/ exported symbols are importable by other modules; internal/
packages and _test.go files are not). go build, go vet and test-compile all pass.
Retire runtime/API/UI monitored-system volume enforcement now that infrastructure monitoring is no longer capped.
Keep only legacy metadata scrubbing and purchase-start compatibility for old max_monitored_systems references.
Rename the remaining preview surface to monitored-system impact and make previews explanatory rather than save-blocking.
Update subsystem contracts and RA7 evidence for the caps-retired invariant.
Treat OIDC, SAML, and multi-provider SSO as included Community capabilities while retaining advanced_sso as a compatibility key. Remove SAML-specific paywalls and paid-upgrade copy from runtime, settings UI, entitlement snapshots, docs, journey proof, and subsystem contracts.
Refs #1449
Remove local upgrade-metrics API registration, settings payload wiring, startup store migration, and backend conversion recorder hooks from the normal product runtime.
Delete the retired conversion/funnel and metering packages from compiled licensing code, and extend diagnostics boundary audits and governance contracts so maintainer commercial analytics cannot return through Settings or diagnostics.