Repository: open-telemetry/opamp-go
Pull request #408 (2025-06-30 18:31:44)

chore(deps): update github/codeql-action action to v3.29.2
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github/codeql-action](https://redirect.github.com/github/codeql-action) | action | patch | `v3.29.1` -> `v3.29.2` |

---

### Release Notes

<details>
<summary>github/codeql-action (github/codeql-action)</summary>

### [`v3.29.2`](https://redirect.github.com/github/codeql-action/compare/v3.29.1...v3.29.2)

[Compare Source](https://redirect.github.com/github/codeql-action/compare/v3.29.1...v3.29.2)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/open-telemetry/opamp-go).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MC42Mi4xIiwidXBkYXRlZEluVmVyIjoiNDAuNjIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->



Patch:
diff --git a/.github/workflows/ossf-scorecard.yml b/.github/workflows/ossf-scorecard.yml
index 7439ae6..9f1beeb 100644
--- a/.github/workflows/ossf-scorecard.yml
+++ b/.github/workflows/ossf-scorecard.yml
@@ -42,6 +42,6 @@ jobs:
       # Upload the results to GitHub's code scanning dashboard (optional).
       # Commenting out will disable upload of results to your repo's Code Scanning dashboard
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@39edc492dbe16b1465b0cafca41432d857bdb31a # v3.29.1
+        uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
         with:
           sarif_file: results.sarif
diff --git a/.github/workflows/static-check.yml b/.github/workflows/static-check.yml
index 959c60a..95d4f23 100644
--- a/.github/workflows/static-check.yml
+++ b/.github/workflows/static-check.yml
@@ -9,6 +9,9 @@ on:
     branches:
       - main
 
+permissions:
+  contents: read
+
 jobs:
   staticcheck:
     name: staticcheck (project)
@@ -24,7 +27,7 @@ jobs:
         uses: WillAbides/setup-go-faster@v1.14.0
         with:
           go-version: "1.22.x"
-      - uses: dominikh/staticcheck-action@v1.3.1
+      - uses: dominikh/staticcheck-action@v1.4.0
         with:
           install-go: false
           version: "2023.1.7"
diff --git a/.github/workflows/tidy.yml b/.github/workflows/tidy.yml
index 2fe2c9a..72210ce 100644
--- a/.github/workflows/tidy.yml
+++ b/.github/workflows/tidy.yml
@@ -4,6 +4,8 @@ on:
     types: [opened, ready_for_review, synchronize, reopened, labeled, unlabeled]
     branches:
       - main
+permissions:
+  contents: read
 jobs:
   setup-environment:
     runs-on: ubuntu-latest
diff --git a/client/client.go b/client/client.go
index ae054f3..1e83be2 100644
--- a/client/client.go
+++ b/client/client.go
@@ -152,4 +152,17 @@ type OpAMPClient interface {
 	// This method is subject to agent status compression - if components is not
 	// different from the cached agent state, this method is a no-op.
 	SetAvailableComponents(components *protobufs.AvailableComponents) error
+
+	// SetCapabilities updates the set of capabilities that the client supports.
+	// These capabilities will be communicated to the server in the next message.
+	//
+	// This method can be called at any time before or after Start(), including from within
+	// an OnMessage handler, to dynamically update the set of supported capabilities.
+	// The updated capabilities will be sent to the server in the next outgoing message.
+	//
+	// The capabilities parameter must not be nil; passing a nil value will result in an error.
+	//
+	// For more details, refer to the OpAMP specification:
+	// https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#agenttoservercapabilities
+	SetCapabilities(capabilities *protobufs.AgentCapabilities) error
 }
diff --git a/client/httpclient.go b/client/httpclient.go
index af91147..0024977 100644
--- a/client/httpclient.go
+++ b/client/httpclient.go
@@ -10,6 +10,8 @@ import (
 	"github.com/open-telemetry/opamp-go/protobufs"
 )
 
+var _ OpAMPClient = (*httpClient)(nil)
+
 // httpClient is an OpAMP Client implementation for plain HTTP transport.
 // See specification: https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#plain-http-transport
 type httpClient struct {
@@ -125,6 +127,11 @@ func (c *httpClient) SetAvailableComponents(components *protobufs.AvailableCompo
 	return c.common.SetAvailableComponents(components)
 }
 
+// SetCapabilities implements OpAMPClient.SetCapabilities
+func (c *httpClient) SetCapabilities(capabilities *protobufs.AgentCapabilities) error {
+	return c.common.SetCapabilities(capabilities)
+}
+
 func (c *httpClient) runUntilStopped(ctx context.Context) {
 	// Start the HTTP sender. This will make request/responses with retries for
 	// failures and will wait with configured polling interval if there is nothing
@@ -135,7 +142,6 @@ func (c *httpClient) runUntilStopped(ctx context.Context) {
 		c.common.Callbacks,
 		&c.common.ClientSyncedState,
 		c.common.PackagesStateProvider,
-		c.common.Capabilities,
 		&c.common.PackageSyncMutex,
 		c.common.DownloadReporterInterval,
 	)
diff --git a/client/internal/clientcommon.go b/client/internal/clientcommon.go
index 9f1bf16..a0f5d3a 100644
--- a/client/internal/clientcommon.go
+++ b/client/internal/clientcommon.go
@@ -20,6 +20,7 @@ var (
 	ErrReportsEffectiveConfigNotSet = errors.New("ReportsEffectiveConfig capability is not set")
 	ErrReportsRemoteConfigNotSet    = errors.New("ReportsRemoteConfig capability is not set")
 	ErrPackagesStateProviderNotSet  = errors.New("PackagesStateProvider must be set")
+	ErrCapabilitiesNotSet           = errors.New("Capabilities is not set")
 	ErrAcceptsPackagesNotSet        = errors.New("AcceptsPackages and ReportsPackageStatuses must be set")
 	ErrAvailableComponentsMissing   = errors.New("AvailableComponents is nil")
 
@@ -34,9 +35,6 @@ type ClientCommon struct {
 	Logger    types.Logger
 	Callbacks types.Callbacks
 
-	// Agent's capabilities defined at Start() time.
-	Capabilities protobufs.AgentCapabilities
-
 	// Client state storage. This is needed if the Server asks to report the state.
 	ClientSyncedState ClientSyncedState
 
@@ -72,30 +70,68 @@ func NewClientCommon(logger types.Logger, sender Sender) ClientCommon {
 	return ClientCommon{Logger: logger, sender: sender, stoppedSignal: make(chan struct{}, 1)}
 }
 
+func (c *ClientCommon) hasCapability(capability protobufs.AgentCapabilities) bool {
+	return c.ClientSyncedState.Capabilities()&capability != 0
+}
+
+func (c *ClientCommon) validateCapabilities(capabilities protobufs.AgentCapabilities) error {
+	if capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth != 0 && c.ClientSyncedState.Health() == nil {
+		return ErrHealthMissing
+	}
+	if capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents != 0 && c.ClientSyncedState.AvailableComponents() == nil {
+		return ErrAvailableComponentsMissing
+	}
+	if c.PackagesStateProvider != nil {
+		if (capabilities&protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages == 0) ||
+			(capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses == 0) {
+			return ErrAcceptsPackagesNotSet
+		}
+	} else {
+		if capabilities&protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages != 0 ||
+			capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses != 0 {
+			return ErrPackagesStateProviderNotSet
+		}
+	}
+	return nil
+}
+
 // PrepareStart prepares the client state for the next Start() call.
 // It returns an error if the client is already started, or if the settings are invalid.
 func (c *ClientCommon) PrepareStart(
-	_ context.Context, settings types.StartSettings,
+	ctx context.Context, settings types.StartSettings,
 ) error {
 	if c.isStarted {
 		return errAlreadyStarted
 	}
-
-	c.Capabilities = settings.Capabilities
-
-	// According to OpAMP spec this capability MUST be set, since all Agents MUST report status.
-	c.Capabilities |= protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus
+	// Deprecated: Use client.SetCapabilities() instead.
+	if settings.Capabilities != 0 {
+		c.Logger.Errorf(ctx, "settings.Capabilities is deprecated, use client.SetCapabilities() instead")
+		capabilities := settings.Capabilities
+		// According to OpAMP spec this capability MUST be set, since all Agents MUST report status.
+		capabilities |= protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus
+		if err := c.ClientSyncedState.SetCapabilities(&capabilities); err != nil {
+			return err
+		}
+	}
+	if c.ClientSyncedState.Capabilities() == 0 {
+		c.Logger.Errorf(ctx, "you must call client.SetCapabilities before start.")
+		capabilities := protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus
+		if err := c.ClientSyncedState.SetCapabilities(&capabilities); err != nil {
+			return err
+		}
+		// We allow this to succeed for now, but later this will become an error.
+		// TODO: https://github.com/open-telemetry/opamp-go/issues/407
+		// return ErrCapabilitiesNotSet
+	}
 
 	if c.ClientSyncedState.AgentDescription() == nil {
 		return ErrAgentDescriptionMissing
 	}
 
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth != 0 && c.ClientSyncedState.Health() == nil {
-		return ErrHealthMissing
-	}
-
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents != 0 && c.ClientSyncedState.AvailableComponents() == nil {
-		return ErrAvailableComponentsMissing
+	// Prepare package statuses.
+	c.PackagesStateProvider = settings.PackagesStateProvider
+	if err := c.validateCapabilities(c.ClientSyncedState.Capabilities()); err != nil {
+		return err
 	}
 
 	// Prepare remote config status.
@@ -110,26 +146,16 @@ func (c *ClientCommon) PrepareStart(
 		return err
 	}
 
-	// Prepare package statuses.
-	c.PackagesStateProvider = settings.PackagesStateProvider
 	var packageStatuses *protobufs.PackageStatuses
-	if settings.PackagesStateProvider != nil {
-		if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages == 0 ||
-			c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses == 0 {
-			return ErrAcceptsPackagesNotSet
-		}
-
+	if c.PackagesStateProvider != nil &&
+		c.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages) &&
+		c.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses) {
 		// Set package status from the value previously saved in the PackagesStateProvider.
 		var err error
 		packageStatuses, err = settings.PackagesStateProvider.LastReportedStatuses()
 		if err != nil {
 			return err
 		}
-	} else {
-		if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages != 0 ||
-			c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses != 0 {
-			return ErrPackagesStateProviderNotSet
-		}
 	}
 
 	if packageStatuses == nil {
@@ -144,7 +170,7 @@ func (c *ClientCommon) PrepareStart(
 	c.Callbacks = settings.Callbacks
 	c.Callbacks.SetDefaults()
 
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsHeartbeat != 0 && settings.HeartbeatInterval != nil {
+	if c.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsHeartbeat) && settings.HeartbeatInterval != nil {
 		if err := c.sender.SetHeartbeatInterval(*settings.HeartbeatInterval); err != nil {
 			return err
 		}
@@ -231,7 +257,7 @@ func (c *ClientCommon) PrepareFirstMessage(ctx context.Context) error {
 	// initially, do not send the full component state - just send the hash.
 	// full state is available on request from the server using the corresponding ServerToAgent flag
 	var availableComponents *protobufs.AvailableComponents
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents != 0 {
+	if c.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents) {
 		availableComponents = &protobufs.AvailableComponents{
 			Hash: c.ClientSyncedState.AvailableComponents().GetHash(),
 		}
@@ -243,7 +269,7 @@ func (c *ClientCommon) PrepareFirstMessage(ctx context.Context) error {
 			msg.EffectiveConfig = cfg
 			msg.RemoteConfigStatus = c.ClientSyncedState.RemoteConfigStatus()
 			msg.PackageStatuses = c.ClientSyncedState.PackageStatuses()
-			msg.Capabilities = uint64(c.Capabilities)
+			msg.Capabilities = uint64(c.ClientSyncedState.Capabilities())
 			msg.CustomCapabilities = c.ClientSyncedState.CustomCapabilities()
 			msg.Flags = c.ClientSyncedState.Flags()
 			msg.AvailableComponents = availableComponents
@@ -305,7 +331,7 @@ func (c *ClientCommon) SetHealth(health *protobufs.ComponentHealth) error {
 // UpdateEffectiveConfig fetches the current local effective config using
 // GetEffectiveConfig callback and sends it to the Server using provided Sender.
 func (c *ClientCommon) UpdateEffectiveConfig(ctx context.Context) error {
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig == 0 {
+	if !c.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig) {
 		return ErrReportsEffectiveConfigNotSet
 	}
 
@@ -336,7 +362,7 @@ func (c *ClientCommon) UpdateEffectiveConfig(ctx context.Context) error {
 // It also remembers the new RemoteConfigStatus in the client state so that it can be
 // sent to the Server when the Server asks for it.
 func (c *ClientCommon) SetRemoteConfigStatus(status *protobufs.RemoteConfigStatus) error {
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig == 0 {
+	if !c.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig) {
 		return ErrReportsRemoteConfigNotSet
 	}
 
@@ -371,7 +397,7 @@ func (c *ClientCommon) SetRemoteConfigStatus(status *protobufs.RemoteConfigStatu
 // It also remembers the new PackageStatuses in the client state so that it can be
 // sent to the Server when the Server asks for it.
 func (c *ClientCommon) SetPackageStatuses(statuses *protobufs.PackageStatuses) error {
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses == 0 {
+	if !c.hasCapability(protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses) {
 		return errReportsPackageStatusesNotSet
 	}
 
@@ -466,7 +492,7 @@ func (c *ClientCommon) SetAvailableComponents(components *protobufs.AvailableCom
 		return c.ClientSyncedState.SetAvailableComponents(components)
 	}
 
-	if c.Capabilities&protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents == 0 {
+	if c.ClientSyncedState.Capabilities()&protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents == 0 {
 		return types.ErrReportsAvailableComponentsNotSet
 	}
 
@@ -503,3 +529,27 @@ func (c *ClientCommon) SetAvailableComponents(components *protobufs.AvailableCom
 
 	return nil
 }
+
+// SetCapabilities sends a message to the Server with the new capabilities.
+func (c *ClientCommon) SetCapabilities(capabilities *protobufs.AgentCapabilities) error {
+	if capabilities == nil {
+		return nil
+	}
+	// According to OpAMP spec this capability MUST be set, since all Agents MUST report status.
+	*capabilities |= protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus
+	if validateErr := c.validateCapabilities(*capabilities); validateErr != nil {
+		return validateErr
+	}
+	// store the capabilities to send
+	if err := c.ClientSyncedState.SetCapabilities(capabilities); err != nil {
+		return err
+	}
+	// send the new customCapabilities to the Server
+	c.sender.NextMessage().Update(
+		func(msg *protobufs.AgentToServer) {
+			msg.Capabilities = uint64(c.ClientSyncedState.Capabilities())
+		},
+	)
+	c.sender.ScheduleSend()
+	return nil
+}
diff --git a/client/internal/clientstate.go b/client/internal/clientstate.go
index ae3da41..b672719 100644
--- a/client/internal/clientstate.go
+++ b/client/internal/clientstate.go
@@ -42,6 +42,7 @@ type ClientSyncedState struct {
 	customCapabilities  *protobufs.CustomCapabilities
 	availableComponents *protobufs.AvailableComponents
 	flags               protobufs.AgentToServerFlags
+	agentCapabilities   protobufs.AgentCapabilities
 }
 
 func (s *ClientSyncedState) AgentDescription() *protobufs.AgentDescription {
@@ -86,6 +87,12 @@ func (s *ClientSyncedState) Flags() uint64 {
 	return uint64(s.flags)
 }
 
+func (s *ClientSyncedState) Capabilities() protobufs.AgentCapabilities {
+	defer s.mutex.Unlock()
+	s.mutex.Lock()
+	return s.agentCapabilities
+}
+
 // SetAgentDescription sets the AgentDescription in the state.
 func (s *ClientSyncedState) SetAgentDescription(descr *protobufs.AgentDescription) error {
 	if descr == nil {
@@ -205,3 +212,16 @@ func (s *ClientSyncedState) SetFlags(flags protobufs.AgentToServerFlags) {
 
 	s.flags = flags
 }
+
+// SetCapabilities sets the Capabilities in the state.
+func (s *ClientSyncedState) SetCapabilities(capabilities *protobufs.AgentCapabilities) error {
+	if capabilities == nil {
+		return ErrCapabilitiesNotSet
+	}
+
+	defer s.mutex.Unlock()
+	s.mutex.Lock()
+	s.agentCapabilities = *capabilities
+
+	return nil
+}
diff --git a/client/internal/httpsender.go b/client/internal/httpsender.go
index b5b9d93..a8c7996 100644
--- a/client/internal/httpsender.go
+++ b/client/internal/httpsender.go
@@ -95,13 +95,12 @@ func (h *HTTPSender) Run(
 	callbacks types.Callbacks,
 	clientSyncedState *ClientSyncedState,
 	packagesStateProvider types.PackagesStateProvider,
-	capabilities protobufs.AgentCapabilities,
 	packageSyncMutex *sync.Mutex,
 	reporterInterval time.Duration,
 ) {
 	h.url = url
 	h.callbacks = callbacks
-	h.receiveProcessor = newReceivedProcessor(h.logger, callbacks, h, clientSyncedState, packagesStateProvider, capabilities, packageSyncMutex, reporterInterval)
+	h.receiveProcessor = newReceivedProcessor(h.logger, callbacks, h, clientSyncedState, packagesStateProvider, packageSyncMutex, reporterInterval)
 
 	// we need to detect if the redirect was ever set, if not, we want default behaviour
 	if callbacks.CheckRedirect != nil {
diff --git a/client/internal/receivedprocessor.go b/client/internal/receivedprocessor.go
index 5e53b71..568212d 100644
--- a/client/internal/receivedprocessor.go
+++ b/client/internal/receivedprocessor.go
@@ -29,9 +29,6 @@ type receivedProcessor struct {
 	// packageSyncMutex protects against multiple package syncing operations at the same time.
 	packageSyncMutex *sync.Mutex
 
-	// Agent's capabilities defined at Start() time.
-	capabilities protobufs.AgentCapabilities
-
 	// Download reporter interval value
 	// a negative number indicates that the default should be used instead.
 	downloadReporterInt time.Duration
@@ -43,7 +40,6 @@ func newReceivedProcessor(
 	sender Sender,
 	clientSyncedState *ClientSyncedState,
 	packagesStateProvider types.PackagesStateProvider,
-	capabilities protobufs.AgentCapabilities,
 	packageSyncMutex *sync.Mutex,
 	downloadReporterInt time.Duration,
 ) receivedProcessor {
@@ -53,7 +49,6 @@ func newReceivedProcessor(
 		sender:                sender,
 		clientSyncedState:     clientSyncedState,
 		packagesStateProvider: packagesStateProvider,
-		capabilities:          capabilities,
 		packageSyncMutex:      packageSyncMutex,
 		downloadReporterInt:   downloadReporterInt,
 	}
@@ -185,7 +180,7 @@ func (r *receivedProcessor) ProcessReceivedMessage(ctx context.Context, msg *pro
 }
 
 func (r *receivedProcessor) hasCapability(capability protobufs.AgentCapabilities) bool {
-	return r.capabilities&capability != 0
+	return r.clientSyncedState.Capabilities()&capability != 0
 }
 
 func (r *receivedProcessor) rcvFlags(
@@ -204,6 +199,7 @@ func (r *receivedProcessor) rcvFlags(
 
 		r.sender.NextMessage().Update(
 			func(msg *protobufs.AgentToServer) {
+				msg.Capabilities = uint64(r.clientSyncedState.Capabilities())
 				msg.AgentDescription = r.clientSyncedState.AgentDescription()
 				msg.Health = r.clientSyncedState.Health()
 				msg.RemoteConfigStatus = r.clientSyncedState.RemoteConfigStatus()
diff --git a/client/internal/wsreceiver.go b/client/internal/wsreceiver.go
index 3b2d482..44ce989 100644
--- a/client/internal/wsreceiver.go
+++ b/client/internal/wsreceiver.go
@@ -33,7 +33,6 @@ func NewWSReceiver(
 	sender *WSSender,
 	clientSyncedState *ClientSyncedState,
 	packagesStateProvider types.PackagesStateProvider,
-	capabilities protobufs.AgentCapabilities,
 	packageSyncMutex *sync.Mutex,
 	reporterInterval time.Duration,
 ) *wsReceiver {
@@ -42,7 +41,7 @@ func NewWSReceiver(
 		logger:    logger,
 		sender:    sender,
 		callbacks: callbacks,
-		processor: newReceivedProcessor(logger, callbacks, sender, clientSyncedState, packagesStateProvider, capabilities, packageSyncMutex, reporterInterval),
+		processor: newReceivedProcessor(logger, callbacks, sender, clientSyncedState, packagesStateProvider, packageSyncMutex, reporterInterval),
 		stopped:   make(chan struct{}),
 	}
 
diff --git a/client/types/startsettings.go b/client/types/startsettings.go
index c8ba7c1..ecfabfd 100644
--- a/client/types/startsettings.go
+++ b/client/types/startsettings.go
@@ -48,6 +48,7 @@ type StartSettings struct {
 
 	// Defines the capabilities of the Agent. AgentCapabilities_ReportsStatus bit does not need to
 	// be set in this field, it will be set automatically since it is required by OpAMP protocol.
+	// Deprecated: Use client.SetCapabilities() instead.
 	Capabilities protobufs.AgentCapabilities
 
 	// EnableCompression can be set to true to enable the compression. Note that for WebSocket transport
diff --git a/client/wsclient.go b/client/wsclient.go
index 99637a6..ecf782a 100644
--- a/client/wsclient.go
+++ b/client/wsclient.go
@@ -22,6 +22,8 @@ const (
 	defaultShutdownTimeout = 5 * time.Second
 )
 
+var _ OpAMPClient = (*wsClient)(nil)
+
 // wsClient is an OpAMP Client implementation for WebSocket transport.
 // See specification: https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#websocket-transport
 type wsClient struct {
@@ -162,6 +164,11 @@ func (c *wsClient) SetAvailableComponents(components *protobufs.AvailableCompone
 	return c.common.SetAvailableComponents(components)
 }
 
+// SetCapabilities implements OpAMPClient.
+func (c *wsClient) SetCapabilities(capabilities *protobufs.AgentCapabilities) error {
+	return c.common.SetCapabilities(capabilities)
+}
+
 func viaReq(resps []*http.Response) []*http.Request {
 	reqs := make([]*http.Request, 0, len(resps))
 	for _, resp := range resps {
@@ -362,7 +369,6 @@ func (c *wsClient) runOneCycle(ctx context.Context) {
 		c.sender,
 		&c.common.ClientSyncedState,
 		c.common.PackagesStateProvider,
-		c.common.Capabilities,
 		&c.common.PackageSyncMutex,
 		c.common.DownloadReporterInterval,
 	)


Test patch:
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 58e9141..bcb7d58 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -7,6 +7,9 @@ on:
   pull_request:
     types: [opened, synchronize, reopened, labeled, unlabeled]
 
+permissions:
+  contents: read
+
 jobs:
   setup-environment:
     runs-on: ubuntu-latest
diff --git a/client/clientimpl_test.go b/client/clientimpl_test.go
index 785134d..c21fe4d 100644
--- a/client/clientimpl_test.go
+++ b/client/clientimpl_test.go
@@ -25,6 +25,8 @@ import (
 
 const retryAfterHTTPHeader = "Retry-After"
 
+var coreCapabilities = protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus
+
 func createAgentDescr() *protobufs.AgentDescription {
 	agentDescr := &protobufs.AgentDescription{
 		IdentifyingAttributes: []*protobufs.KeyValue{
@@ -129,6 +131,11 @@ func prepareClient(t *testing.T, settings *types.StartSettings, c OpAMPClient) {
 	prepareSettings(t, settings, c)
 	err := c.SetAgentDescription(createAgentDescr())
 	assert.NoError(t, err)
+	if settings.Capabilities != 0 {
+		c.SetCapabilities(&settings.Capabilities)
+	} else {
+		c.SetCapabilities(&coreCapabilities)
+	}
 }
 
 func startClient(t *testing.T, settings types.StartSettings, client OpAMPClient) {
@@ -218,6 +225,8 @@ func TestStopCancellation(t *testing.T) {
 
 func TestStartNoDescription(t *testing.T) {
 	testClients(t, func(t *testing.T, client OpAMPClient) {
+		setErr := client.SetCapabilities(&coreCapabilities)
+		assert.NoError(t, setErr)
 		settings := createNoServerSettings()
 		prepareSettings(t, &settings, client)
 		err := client.Start(context.Background(), settings)
@@ -225,6 +234,41 @@ func TestStartNoDescription(t *testing.T) {
 	})
 }
 
+func TestStartNoCapabilities(t *testing.T) {
+	testClients(t, func(t *testing.T, client OpAMPClient) {
+		setErr := client.SetAgentDescription(createAgentDescr())
+		require.NoError(t, setErr)
+		settings := createNoServerSettings()
+		prepareSettings(t, &settings, client)
+		err := client.Start(context.Background(), settings)
+		assert.NoError(t, err, "no error should be found until the client rejects empty capabilities")
+	})
+}
+
+func TestSetCapabilitiesErrorsBeforeStart(t *testing.T) {
+	testClients(t, func(t *testing.T, client OpAMPClient) {
+		capabilities := coreCapabilities | protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents
+		setCapabilityErr := client.SetCapabilities(&capabilities)
+		assert.Error(t, setCapabilityErr)
+		assert.Contains(t, setCapabilityErr.Error(), "AvailableComponents is nil")
+	})
+}
+
+func TestSetCapabilitiesErrorsDuringStart(t *testing.T) {
+	testClients(t, func(t *testing.T, client OpAMPClient) {
+		capabilities := coreCapabilities | protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents
+		settings := types.StartSettings{
+			Capabilities: capabilities,
+		}
+		prepareClient(t, &settings, client)
+
+		// Client --->
+		startErr := client.Start(context.Background(), settings)
+		assert.Error(t, startErr)
+		assert.Contains(t, startErr.Error(), "AvailableComponents is nil")
+	})
+}
+
 func TestSetInvalidAgentDescription(t *testing.T) {
 	testClients(t, func(t *testing.T, client OpAMPClient) {
 		settings := createNoServerSettings()
@@ -611,7 +655,8 @@ func TestSetEffectiveConfig(t *testing.T) {
 
 		// Now change the config.
 		sendConfig.ConfigMap.ConfigMap["key2"] = &protobufs.AgentConfigFile{}
-		_ = client.UpdateEffectiveConfig(context.Background())
+		updateErr := client.UpdateEffectiveConfig(context.Background())
+		require.NoError(t, updateErr)
 
 		// Verify change is delivered.
 		eventually(
@@ -1657,7 +1702,6 @@ func TestMissingPackagesStateProvider(t *testing.T) {
 				protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses,
 		}
 		prepareClient(t, &settings, client)
-
 		assert.ErrorIs(t, client.Start(context.Background(), settings), internal.ErrPackagesStateProviderNotSet)
 
 		// Start a client.
@@ -1774,6 +1818,98 @@ func TestOfferUpdatedVersion(t *testing.T) {
 	})
 }
 
+func TestSetCapabilities(t *testing.T) {
+	testClients(t, func(t *testing.T, client OpAMPClient) {
+		// Start a Server.
+		srv := internal.StartMockServer(t)
+		srv.EnableExpectMode()
+
+		var clientRcvCustomMessage atomic.Value
+
+		// Start a client.
+		settings := types.StartSettings{
+			OpAMPServerURL: "ws://" + srv.Endpoint,
+			Callbacks: types.Callbacks{
+				OnMessage: func(ctx context.Context, msg *types.MessageData) {
+					clientRcvCustomMessage.Store(msg.CustomMessage)
+				},
+			},
+			Capabilities: protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig,
+		}
+		prepareClient(t, &settings, client)
+
+		// Client --->
+		assert.NoError(t, client.Start(context.Background(), settings))
+
+		// ---> Server
+		srv.Expect(func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent {
+			assert.EqualValues(t, 0, msg.SequenceNum)
+			// The first status report after Start must have the ReportsStatus.
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus != 0)
+			// The first status report after Start must have the ReportsEffectiveConfig.
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig != 0)
+			// The first status report after Start must not  have the AcceptsRemoteConfig.
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig == 0)
+			return &protobufs.ServerToAgent{
+				InstanceUid: msg.InstanceUid,
+			}
+		})
+
+		newCapabilities := protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig |
+			protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig |
+			protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus
+		err := client.SetCapabilities(&newCapabilities)
+		assert.NoError(t, err)
+
+		// ---> Server
+		srv.Expect(func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent {
+			// Check ReportsStatus is still true.
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus != 0)
+			// ReportsEffectiveConfig should no longer be present.
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig != 0)
+			// AcceptsRemoteConfig should now be present
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig != 0)
+
+			// Send a custom message response and ask client for full state again.
+			return &protobufs.ServerToAgent{
+				InstanceUid: msg.InstanceUid,
+				Flags:       uint64(protobufs.ServerToAgentFlags_ServerToAgentFlags_ReportFullState),
+			}
+		})
+
+		newCapabilities = protobufs.AgentCapabilities_AgentCapabilities_AcceptsRestartCommand |
+			protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig |
+			protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig |
+			protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus
+		newSetErr := client.SetCapabilities(&newCapabilities)
+		assert.NoError(t, newSetErr)
+
+		// ---> Server
+		srv.Expect(func(msg *protobufs.AgentToServer) *protobufs.ServerToAgent {
+			// Check ReportsStatus is still true.
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_ReportsStatus != 0)
+			// ReportsEffectiveConfig should  present.
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig != 0)
+			// AcceptsRemoteConfig should now be present
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig != 0)
+			// AcceptsRestartCommand should now be present
+			assert.True(t, protobufs.AgentCapabilities(msg.Capabilities)&protobufs.AgentCapabilities_AgentCapabilities_AcceptsRestartCommand != 0)
+			// Send a custom message response and ask client for full state again.
+			return &protobufs.ServerToAgent{
+				InstanceUid: msg.InstanceUid,
+				Flags:       uint64(protobufs.ServerToAgentFlags_ServerToAgentFlags_ReportFullState),
+			}
+		})
+
+		// Shutdown the Server.
+		srv.Close()
+
+		// Shutdown the client.
+		err = client.Stop(context.Background())
+		assert.NoError(t, err)
+	})
+}
+
 func TestReportCustomCapabilities(t *testing.T) {
 	testClients(t, func(t *testing.T, client OpAMPClient) {
 		// Start a Server.
@@ -2443,6 +2579,95 @@ func TestSetAvailableComponents(t *testing.T) {
 	}
 }
 
+func TestValidateCapabilities(t *testing.T) {
+	testCases := []struct {
+		name          string
+		capabilities  protobufs.AgentCapabilities
+		setupFunc     func(t *testing.T, client OpAMPClient)
+		expectedError error
+	}{
+		{
+			name:         "ReportsHealth capability without health",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				// Do not set health
+			},
+			expectedError: internal.ErrHealthMissing,
+		},
+		{
+			name:         "ReportsHealth capability with health",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_ReportsHealth,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				err := client.SetHealth(&protobufs.ComponentHealth{})
+				require.NoError(t, err)
+			},
+			expectedError: nil,
+		},
+		{
+			name:         "ReportsAvailableComponents capability without available components",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				// Do not set available components
+			},
+			expectedError: internal.ErrAvailableComponentsMissing,
+		},
+		{
+			name:         "ReportsAvailableComponents capability with available components",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_ReportsAvailableComponents,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				err := client.SetAvailableComponents(generateTestAvailableComponents())
+				require.NoError(t, err)
+			},
+			expectedError: nil,
+		},
+		{
+			name:         "AcceptsPackages capability without PackagesStateProvider",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				// Do not set PackagesStateProvider
+			},
+			expectedError: internal.ErrPackagesStateProviderNotSet,
+		},
+		{
+			name:         "ReportsPackageStatuses capability without PackagesStateProvider",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				// Do not set PackagesStateProvider
+			},
+			expectedError: internal.ErrPackagesStateProviderNotSet,
+		},
+		{
+			name:         "AcceptsPackages and ReportsPackageStatuses capabilities without PackagesStateProvider",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages | protobufs.AgentCapabilities_AgentCapabilities_ReportsPackageStatuses,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				// Do not set PackagesStateProvider
+			},
+			expectedError: internal.ErrPackagesStateProviderNotSet,
+		},
+		{
+			name:         "No capabilities set",
+			capabilities: protobufs.AgentCapabilities_AgentCapabilities_Unspecified,
+			setupFunc: func(t *testing.T, client OpAMPClient) {
+				// No setup needed
+			},
+			expectedError: nil,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			testClients(t, func(t *testing.T, client OpAMPClient) {
+				// Setup the client state as per the test case
+				tc.setupFunc(t, client)
+
+				// Validate capabilities
+				err := client.SetCapabilities(&tc.capabilities)
+				assert.Equal(t, tc.expectedError, err)
+			})
+		})
+	}
+}
+
 func generateTestAvailableComponents() *protobufs.AvailableComponents {
 	return &protobufs.AvailableComponents{
 		Hash: []byte("fake-hash"),
diff --git a/client/internal/httpsender_test.go b/client/internal/httpsender_test.go
index c6a3f86..75fff53 100644
--- a/client/internal/httpsender_test.go
+++ b/client/internal/httpsender_test.go
@@ -201,7 +201,8 @@ func TestRequestInstanceUidFlagReset(t *testing.T) {
 	clientSyncedState := &ClientSyncedState{}
 	clientSyncedState.SetFlags(protobufs.AgentToServerFlags_AgentToServerFlags_RequestInstanceUid)
 	capabilities := protobufs.AgentCapabilities_AgentCapabilities_Unspecified
-	sender.receiveProcessor = newReceivedProcessor(&sharedinternal.NopLogger{}, sender.callbacks, sender, clientSyncedState, nil, capabilities, new(sync.Mutex), time.Second)
+	clientSyncedState.SetCapabilities(&capabilities)
+	sender.receiveProcessor = newReceivedProcessor(&sharedinternal.NopLogger{}, sender.callbacks, sender, clientSyncedState, nil, new(sync.Mutex), time.Second)
 
 	// If we process a message with a nil AgentIdentification, or an incorrect NewInstanceUid.
 	sender.receiveProcessor.ProcessReceivedMessage(ctx,
@@ -258,7 +259,8 @@ func TestPackageUpdatesInParallel(t *testing.T) {
 
 	clientSyncedState := &ClientSyncedState{}
 	capabilities := protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages
-	sender.receiveProcessor = newReceivedProcessor(&sharedinternal.NopLogger{}, sender.callbacks, sender, clientSyncedState, localPackageState, capabilities, &mux, time.Second)
+	clientSyncedState.SetCapabilities(&capabilities)
+	sender.receiveProcessor = newReceivedProcessor(&sharedinternal.NopLogger{}, sender.callbacks, sender, clientSyncedState, localPackageState, &mux, time.Second)
 
 	sender.receiveProcessor.ProcessReceivedMessage(ctx,
 		&protobufs.ServerToAgent{
@@ -331,9 +333,10 @@ func TestPackageUpdatesWithError(t *testing.T) {
 	sender.callbacks = callbacks
 
 	clientSyncedState := &ClientSyncedState{}
-
 	capabilities := protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages
-	sender.receiveProcessor = newReceivedProcessor(&sharedinternal.NopLogger{}, sender.callbacks, sender, clientSyncedState, localPackageState, capabilities, &mux, time.Second)
+	clientSyncedState.SetCapabilities(&capabilities)
+
+	sender.receiveProcessor = newReceivedProcessor(&sharedinternal.NopLogger{}, sender.callbacks, sender, clientSyncedState, localPackageState, &mux, time.Second)
 
 	// Send two messages in parallel.
 	sender.receiveProcessor.ProcessReceivedMessage(ctx,
diff --git a/client/internal/wsreceiver_test.go b/client/internal/wsreceiver_test.go
index 0f0fef7..d6d5a31 100644
--- a/client/internal/wsreceiver_test.go
+++ b/client/internal/wsreceiver_test.go
@@ -88,7 +88,8 @@ func TestServerToAgentCommand(t *testing.T) {
 			}
 			sender := WSSender{}
 			capabilities := protobufs.AgentCapabilities_AgentCapabilities_AcceptsRestartCommand
-			receiver := NewWSReceiver(TestLogger{t}, callbacks, nil, &sender, &clientSyncedState, nil, capabilities, new(sync.Mutex), time.Second)
+			clientSyncedState.SetCapabilities(&capabilities)
+			receiver := NewWSReceiver(TestLogger{t}, callbacks, nil, &sender, &clientSyncedState, nil, new(sync.Mutex), time.Second)
 			receiver.processor.ProcessReceivedMessage(context.Background(), &protobufs.ServerToAgent{
 				Command: test.command,
 			})
@@ -142,7 +143,8 @@ func TestServerToAgentCommandExclusive(t *testing.T) {
 			},
 		}
 		clientSyncedState := ClientSyncedState{}
-		receiver := NewWSReceiver(TestLogger{t}, callbacks, nil, nil, &clientSyncedState, nil, test.capabilities, new(sync.Mutex), time.Second)
+		clientSyncedState.SetCapabilities(&test.capabilities)
+		receiver := NewWSReceiver(TestLogger{t}, callbacks, nil, nil, &clientSyncedState, nil, new(sync.Mutex), time.Second)
 		receiver.processor.ProcessReceivedMessage(context.Background(), &protobufs.ServerToAgent{
 			Command: &protobufs.ServerToAgentCommand{
 				Type: protobufs.CommandType_CommandType_Restart,
@@ -204,7 +206,8 @@ func TestReceiverLoopStop(t *testing.T) {
 	}
 	sender := WSSender{}
 	capabilities := protobufs.AgentCapabilities_AgentCapabilities_AcceptsRestartCommand
-	receiver := NewWSReceiver(TestLogger{t}, callbacks, conn, &sender, &clientSyncedState, nil, capabilities, new(sync.Mutex), time.Second)
+	clientSyncedState.SetCapabilities(&capabilities)
+	receiver := NewWSReceiver(TestLogger{t}, callbacks, conn, &sender, &clientSyncedState, nil, new(sync.Mutex), time.Second)
 	ctx, cancel := context.WithCancel(context.Background())
 
 	go func() {
@@ -246,7 +249,8 @@ func TestWSPackageUpdatesInParallel(t *testing.T) {
 	clientSyncedState := &ClientSyncedState{}
 	capabilities := protobufs.AgentCapabilities_AgentCapabilities_AcceptsPackages
 	sender := NewSender(&internal.NopLogger{})
-	receiver := NewWSReceiver(&internal.NopLogger{}, callbacks, nil, sender, clientSyncedState, localPackageState, capabilities, &mux, time.Second)
+	clientSyncedState.SetCapabilities(&capabilities)
+	receiver := NewWSReceiver(&internal.NopLogger{}, callbacks, nil, sender, clientSyncedState, localPackageState, &mux, time.Second)
 
 	receiver.processor.ProcessReceivedMessage(ctx,
 		&protobufs.ServerToAgent{