Skip to content

Refactor secret management client methods and update configurations#680

Merged
hanzjk merged 7 commits intowso2:mainfrom
hanzjk:secret-seup
Apr 9, 2026
Merged

Refactor secret management client methods and update configurations#680
hanzjk merged 7 commits intowso2:mainfrom
hanzjk:secret-seup

Conversation

@hanzjk
Copy link
Copy Markdown
Contributor

@hanzjk hanzjk commented Apr 8, 2026

Purpose

Describe the problems, issues, or needs driving this feature/fix and include links to related issues in the following format: Resolves issue1, issue2, etc.

Resolves #685

Goals

Describe the solutions that this feature/fix will introduce to resolve the problems described above

Approach

Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email documentation@wso2.com to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here.

User stories

Summary of user stories addressed by this change>

Release note

Brief description of the new feature or bug fix as it will appear in the release notes

Documentation

Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter �N/A� plus brief explanation of why there�s no doc impact

Training

Link to the PR for changes to the training content in https://github.com/wso2/WSO2-Training, if applicable

Certification

Type �Sent� when you have provided new/updated certification questions, plus four answers for each question (correct answer highlighted in bold), based on this change. Certification questions/answers should be sent to certification@wso2.com and NOT pasted in this PR. If there is no impact on certification exams, type �N/A� and explain why.

Marketing

Link to drafts of marketing content that will describe and promote this feature, including product page changes, technical articles, blog posts, videos, etc., if applicable

Automation tests

  • Unit tests

    Code coverage information

  • Integration tests

    Details about the test cases and coverage

Security checks

Samples

Provide high-level details about the samples related to this feature

Related PRs

List any other related PRs

Migrations (if applicable)

Describe migration steps and platforms on which migration has been tested

Test environment

List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested

Learning

Describe the research phase and any blog posts, patterns, libraries, or add-ons you used to solve the problem.

Summary by CodeRabbit

  • New Features

    • Secret patching with merge semantics and explicit key deletions
    • Distinct metadata-only secret fetch and value-inclusive fetch
    • Configurable "secret-manager-api" provider with base URL and timeout
  • Improvements

    • Structured secret locations and deterministic SecretReference naming
    • SecretReference lifecycle and refresh handled by secret manager integration
    • Simplified rollback/cleanup and YAML secret resolution flows
  • Tests

    • Updated test mocks to match the expanded secret client surface

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Secret management APIs and implementations were redesigned: raw KV key strings replaced by structured SecretLocation; reads split into metadata (GetSecret) and value (GetSecretWithValue); PatchSecret added; DeleteSecret now requires secretRefName; SecretReference lifecycle moved into the secret client; mocks, wiring, configs, and call sites updated.

Changes

Cohort / File(s) Summary
Core Interface & Types
agent-manager-service/clients/secretmanagersvc/provider.go, agent-manager-service/clients/secretmanagersvc/types.go
Replaced raw-key methods with SecretLocation-based signatures. PushSecret/PatchSecret now return reference strings. GetSecret returns metadata *SecretInfo; added GetSecretWithValue and ErrNotSupported.
SecretManagement Client
agent-manager-service/clients/secretmanagersvc/client.go
Added SecretManagementClientConfig and NewSecretManagementClientWithConfig; added SecretLocation.SecretRefName() and ParseKVPath; refactored Create/Patch/Delete/Get flows to use SecretLocation, manage SecretReference when ocClient present, split metadata/value reads, and add PatchSecret and updated DeleteSecret signature.
Provider: OpenBao
agent-manager-service/clients/secretmanagersvc/providers/openbao/client.go
Ported provider to accept SecretLocation, return secret ref strings, added PatchSecret (JSON merge semantics), split metadata/value reads (GetSecret vs GetSecretWithValue), renamed readSecretreadSecretValue, and updated delete/ownership logic.
Mocks & Test Utilities
agent-manager-service/clients/clientmocks/secret_mgmt_client_fake.go, agent-manager-service/tests/apitestutils/mock_clients.go
Updated mock signatures and call-tracking: DeleteSecret now accepts secretRefName; removed DeleteSecretByPath; GetSecret returns *SecretInfo; added GetSecretWithValue and PatchSecret; adjusted defaults, calls, and locks.
Service Layer (call sites)
agent-manager-service/services/agent_configuration_service.go, agent-manager-service/services/agent_manager.go, agent-manager-service/services/llm_provider_service.go, agent-manager-service/services/gateway_internal_service.go
Refactored callers to use SecretLocation and SecretRefName() instead of KV paths; services call GetSecretWithValue for values, use PatchSecret for merges, delegate SecretReference lifecycle to the secret client, and updated rollback/cleanup flows.
Configuration & Loader
agent-manager-service/config/config.go, agent-manager-service/config/config_loader.go
Added BaseURL and Timeout to SecretManagerConfig; loader reads SECRET_MANAGER_API_URL and SECRET_MANAGER_API_TIMEOUT.
Dependency Injection / Wiring
agent-manager-service/wiring/wire.go, agent-manager-service/wiring/wire_gen.go
ProvideSecretManagementClient now accepts ocClient and constructs the client via NewSecretManagementClientWithConfig, wiring provider, OC client, and refresh interval.
Other
agent-manager-service/clients/clientmocks/..., agent-manager-service/wiring/...
Various call-site updates, small logging/variable adjustments and removal of OC-owned SecretReference upsert from service helpers.

Sequence Diagram(s)

sequenceDiagram
    participant Service as Agent Configuration Service
    participant SMClient as Secret Management Client
    participant Provider as Secrets Provider (OpenBao)
    participant OC as OpenChoreo Client

    Note over Service,SMClient: Create / Upsert Secret
    Service->>SMClient: CreateSecret(ctx, location, data)
    SMClient->>Provider: PushSecret(ctx, location, value, metadata)
    Provider-->>SMClient: kvPath / secretRef, err
    alt ocClient configured
        SMClient->>OC: GetSecretReference(secretRefName)
        alt not exists
            SMClient->>OC: CreateSecretReference(crd)
        else
            SMClient->>OC: UpdateSecretReference(crd)
        end
    end
    SMClient-->>Service: secretRefName / err
Loading
sequenceDiagram
    participant Service as Gateway / Consumer
    participant SMClient as Secret Management Client
    participant Provider as Secrets Provider (OpenBao)

    Note over Service,SMClient: Read-with-value Flow
    Service->>SMClient: GetSecretWithValue(ctx, kvPath)
    SMClient->>SMClient: ParseKVPath(kvPath) -> SecretLocation
    SMClient->>Provider: GetSecretWithValue(ctx, location)
    Provider-->>SMClient: value ([]byte) / ErrNotSupported / err
    SMClient-->>Service: value / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰
I hopped through paths and trimmed a name,
Split values from metadata—never the same.
Patches stitched keys, CRDs held tight,
References nested, rollback tucked right.
A tiny thump for secrets made bright.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description consists entirely of empty template placeholders with no concrete content, links, or answers filled in. Complete all required sections: Purpose (with issue links), Goals, Approach, User stories, Release note, Documentation, Security checks, and other applicable sections with actual information.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main refactor of secret management client methods and configuration updates shown in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
agent-manager-service/services/agent_configuration_service.go (1)

1856-1873: ⚠️ Potential issue | 🔴 Critical

Keep UpstreamAuth.SecretRef as the KV path.

secretClient.CreateSecret() now returns the OpenChoreo SecretReference name when ocClient is configured. Persisting that value in UpstreamAuth.SecretRef turns a KV-path field into a Kubernetes resource name, so secured providers will no longer be able to resolve their upstream credential.

Suggested fix
-			kvPath, err := s.secretClient.CreateSecret(ctx, *providerSecretLoc,
+			kvPath, err := providerSecretLoc.KVPath()
+			if err != nil {
+				return nil, "", "", nil, fmt.Errorf("failed to derive provider secret KV path: %w", err)
+			}
+			_, err = s.secretClient.CreateSecret(ctx, *providerSecretLoc,
 				map[string]string{secretmanagersvc.SecretKeyAPIKey: apiKey.APIKey})
 			if err != nil {
 				// revoke created api key
 				if err := s.llmProviderAPIKeyService.RevokeAPIKey(ctx, config.OrganizationName, provider.UUID.String(), proxyName); err != nil {
 					s.logger.Error("Failed to revoke provider API key during creation",
Based on learnings, `UpstreamAuth.SecretRef` in `agent-manager-service/models/upstream.go` is a KV path reference managed exclusively by the secret management integration layer.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_configuration_service.go` around lines
1856 - 1873, The code is storing the value returned by secretClient.CreateSecret
(kvPath) into UpstreamAuth.SecretRef, but that return can be an OpenChoreo
SecretReference rather than the KV path that UpstreamAuth.SecretRef expects;
change the assignment so UpstreamAuth.SecretRef keeps the original KV path (the
providerSecretLoc value) instead of kvPath. Update the block around
secretClient.CreateSecret (references: CreateSecret, providerSecretLoc, kvPath,
upstreamAuthConfig.SecretRef, UpstreamAuth) to assign the KV-path string (or
pointer to it) used to create the secret to upstreamAuthConfig.SecretRef and
leave kvPath for any oc-specific handling, ensuring the stored type matches
models/upstream.go expectations.
🧹 Nitpick comments (2)
agent-manager-service/config/config_loader.go (1)

180-185: Consider adding validation for secret-manager-api provider configuration.

The new BaseURL field has an empty default, which is fine for other providers. However, when Provider is set to "secret-manager-api", a missing BaseURL will likely cause runtime failures when the client attempts to make HTTP requests.

Consider adding validation in loadEnvs() or a dedicated validation function:

if cfg.SecretManager.Provider == "secret-manager-api" && cfg.SecretManager.BaseURL == "" {
    r.errors = append(r.errors, fmt.Errorf("SECRET_MANAGER_API_URL is required when SECRET_MANAGER_PROVIDER is 'secret-manager-api'"))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/config/config_loader.go` around lines 180 - 185, Add
validation to ensure SecretManagerConfig.BaseURL is provided when
SecretManagerConfig.Provider == "secret-manager-api": in loadEnvs() (or a
dedicated validateConfig function) check cfg.SecretManager.Provider (or
r.config.SecretManager) and if it equals "secret-manager-api" and
SecretManager.BaseURL (set via r.readOptionalString("SECRET_MANAGER_API_URL",
"")) is empty, append an error to r.errors (or return/collect an error)
indicating SECRET_MANAGER_API_URL is required; this prevents runtime failures
when the secret-manager-api client (the SecretManagerConfig used by the client
constructor) attempts HTTP requests.
agent-manager-service/services/llm_provider_service.go (1)

249-256: Consider handling KVPath() error for more accurate logging.

The KVPath() method returns (string, error), but the error is discarded with _. If KVPath() fails (e.g., due to an empty OrgName), the logged kvPath could be empty or misleading, making manual cleanup harder.

🔧 Suggested improvement
 if delErr := s.secretClient.DeleteSecret(ctx, *secretLoc, secretLoc.SecretRefName()); delErr != nil {
-    kvPath, _ := secretLoc.KVPath()
+    kvPath, kvErr := secretLoc.KVPath()
+    if kvErr != nil {
+        kvPath = fmt.Sprintf("(failed to compute: %v)", kvErr)
+    }
     slog.Error("LLMProviderService.Create: failed to delete orphaned secret after DB failure — manual cleanup required",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/llm_provider_service.go` around lines 249 -
256, The log call in LLMProviderService.Create discards the error from
secretLoc.KVPath(), which can produce an empty/misleading kvPath; call
secretLoc.KVPath() and capture its (path, err) before logging, and if err != nil
include a clear fallback or the error text in the slog.Error fields (e.g.,
"kvPathError": err) so the log contains either the actual path or the KVPath
error alongside orgName, handle, action and delErr; update the block that calls
s.secretClient.DeleteSecret and the subsequent slog.Error to use these captured
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@agent-manager-service/clients/secretmanagersvc/client.go`:
- Around line 144-149: SecretRefName() currently builds the secretref name using
only ConfigName and EnvironmentName when ConfigName is set, causing distinct
secrets (e.g., provider vs proxy) that share the same config/env to collide;
update SecretLocation.SecretRefName to include EntityName as part of the
generated Kubernetes-safe name whenever ConfigName is present (so the format
uses sanitizeForK8sName(l.ConfigName), sanitizeForK8sName(l.EnvironmentName) and
sanitizeForK8sName(l.EntityName) in the name) to ensure CreateSecret() produces
unique SecretReference names per KV path and avoids upserting one reference over
another.
- Around line 383-396: The branch that runs when c.ocClient != nil calls
c.lowLevelClient.GetSecret and then c.upsertSecretReference but still returns
kvPath, causing PatchSecret callers to receive the remote KV path instead of the
updated SecretReference name; change the return to return the SecretReference
name produced by upsertSecretReference (use the value returned by
upsertSecretReference instead of kvPath) so PatchSecret returns the Kubernetes
Secret name after a successful upsert; locate this logic around ocClient,
lowLevelClient.GetSecret, upsertSecretReference and PatchSecret and replace the
final returned value accordingly.
- Around line 171-195: ParseKVPath only accepts 2 or 4 segments but
SecretLocation.KVPath() can produce longer paths including AgentName,
ConfigName, and SecretKey, causing GetSecret/GetSecretWithValue to fail for
provider secrets created in buildLLMProxyConfig. Update ParseKVPath to accept
and parse the full KVPath shape: detect and handle lengths for 2 (org/entity), 4
(org/project/env/entity) and longer forms (e.g.,
org/project/env/entity/agent/config/key) by mapping parts[0]→OrgName,
parts[1]→ProjectName (if present), parts[2]→EnvironmentName (if present),
parts[3]→EntityName, and assign subsequent segments to AgentName, ConfigName,
and SecretKey respectively so ParseKVPath returns a complete SecretLocation
matching SecretLocation.KVPath().

In `@agent-manager-service/services/agent_configuration_service.go`:
- Around line 397-405: The proxy secret is being stored under agent-scoped
EntityName (agentID) causing a mismatch with Delete() which reconstructs the
secret location using proxy.Handle; update the SecretLocation construction (the
proxySecretLoc variable where you instantiate secretmanagersvc.SecretLocation)
to set EntityName: proxy.Handle (instead of agentID) so create and delete paths
match, and make the same change for the other occurrences that construct proxy
secret locations (the other proxySecretLoc instances around the areas noted) to
use proxy.Handle.

In `@agent-manager-service/services/agent_manager.go`:
- Around line 1814-1845: The code currently always calls
s.secretMgmtClient.PatchSecret even when existingInfo is nil; change the flow so
that when GetSecret returns nil (and preservedSecretKeys is empty) you call
s.secretMgmtClient.CreateSecret(ctx, location, newSecretData) to create the
secret and set secretRefName, and only call s.secretMgmtClient.PatchSecret(ctx,
location, newSecretData, keysToDelete) when existingInfo != nil (i.e., when you
computed keysToDelete). Keep the existing error handling for ErrSecretNotFound
and for the preserved-key error path, and ensure you return/create errors from
CreateSecret the same way you handle PatchSecret errors.

---

Outside diff comments:
In `@agent-manager-service/services/agent_configuration_service.go`:
- Around line 1856-1873: The code is storing the value returned by
secretClient.CreateSecret (kvPath) into UpstreamAuth.SecretRef, but that return
can be an OpenChoreo SecretReference rather than the KV path that
UpstreamAuth.SecretRef expects; change the assignment so UpstreamAuth.SecretRef
keeps the original KV path (the providerSecretLoc value) instead of kvPath.
Update the block around secretClient.CreateSecret (references: CreateSecret,
providerSecretLoc, kvPath, upstreamAuthConfig.SecretRef, UpstreamAuth) to assign
the KV-path string (or pointer to it) used to create the secret to
upstreamAuthConfig.SecretRef and leave kvPath for any oc-specific handling,
ensuring the stored type matches models/upstream.go expectations.

---

Nitpick comments:
In `@agent-manager-service/config/config_loader.go`:
- Around line 180-185: Add validation to ensure SecretManagerConfig.BaseURL is
provided when SecretManagerConfig.Provider == "secret-manager-api": in
loadEnvs() (or a dedicated validateConfig function) check
cfg.SecretManager.Provider (or r.config.SecretManager) and if it equals
"secret-manager-api" and SecretManager.BaseURL (set via
r.readOptionalString("SECRET_MANAGER_API_URL", "")) is empty, append an error to
r.errors (or return/collect an error) indicating SECRET_MANAGER_API_URL is
required; this prevents runtime failures when the secret-manager-api client (the
SecretManagerConfig used by the client constructor) attempts HTTP requests.

In `@agent-manager-service/services/llm_provider_service.go`:
- Around line 249-256: The log call in LLMProviderService.Create discards the
error from secretLoc.KVPath(), which can produce an empty/misleading kvPath;
call secretLoc.KVPath() and capture its (path, err) before logging, and if err
!= nil include a clear fallback or the error text in the slog.Error fields
(e.g., "kvPathError": err) so the log contains either the actual path or the
KVPath error alongside orgName, handle, action and delErr; update the block that
calls s.secretClient.DeleteSecret and the subsequent slog.Error to use these
captured values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f7e264c4-acad-4471-996b-29edbda6be6d

📥 Commits

Reviewing files that changed from the base of the PR and between d6b1763 and 7219bc5.

⛔ Files ignored due to path filters (1)
  • agent-manager-service/go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • agent-manager-service/clients/clientmocks/secret_mgmt_client_fake.go
  • agent-manager-service/clients/secretmanagersvc/client.go
  • agent-manager-service/clients/secretmanagersvc/provider.go
  • agent-manager-service/clients/secretmanagersvc/providers/openbao/client.go
  • agent-manager-service/clients/secretmanagersvc/types.go
  • agent-manager-service/config/config.go
  • agent-manager-service/config/config_loader.go
  • agent-manager-service/services/agent_configuration_service.go
  • agent-manager-service/services/agent_manager.go
  • agent-manager-service/services/gateway_internal_service.go
  • agent-manager-service/services/llm_provider_service.go
  • agent-manager-service/tests/apitestutils/mock_clients.go
  • agent-manager-service/wiring/wire.go
  • agent-manager-service/wiring/wire_gen.go

Comment thread agent-manager-service/clients/secretmanagersvc/client.go
Comment thread agent-manager-service/clients/secretmanagersvc/client.go
Comment thread agent-manager-service/clients/secretmanagersvc/client.go Outdated
Comment thread agent-manager-service/services/agent_configuration_service.go
Comment thread agent-manager-service/services/agent_manager.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
agent-manager-service/services/agent_configuration_service.go (2)

1844-1861: ⚠️ Potential issue | 🔴 Critical

Don't persist the returned SecretReference name as UpstreamAuth.SecretRef.

Per agent-manager-service/clients/secretmanagersvc/client.go:1-210, SecretManagementClient.CreateSecret now returns the OpenChoreo secretRefName, while GetSecretWithValue still resolves secrets by KV path. Persisting the CreateSecret return value here will store something like config-env-secrets instead of the provider secret location, so secured upstream auth can no longer resolve the provider API key.

Suggested fix
-			kvPath, err := s.secretClient.CreateSecret(ctx, *providerSecretLoc,
-				map[string]string{secretmanagersvc.SecretKeyAPIKey: apiKey.APIKey})
-			if err != nil {
+			if _, err := s.secretClient.CreateSecret(ctx, *providerSecretLoc,
+				map[string]string{secretmanagersvc.SecretKeyAPIKey: apiKey.APIKey}); err != nil {
 				// revoke created api key
 				if err := s.llmProviderAPIKeyService.RevokeAPIKey(ctx, config.OrganizationName, provider.UUID.String(), proxyName); err != nil {
 					s.logger.Error("Failed to revoke provider API key during creation",
 						"providerUUID", provider.UUID.String(),
 						"providerKeyName", proxyName,
 						"error", err,
 					)
 				}
 				return nil, "", "", nil, fmt.Errorf("failed to store provider API key in KV: %w", err)
 			}
+			kvPath, err := providerSecretLoc.KVPath()
+			if err != nil {
+				return nil, "", "", nil, fmt.Errorf("failed to derive provider KV path: %w", err)
+			}

 			upstreamAuthConfig.Type = utils.StrAsStrPointer(models.AuthTypeAPIKey)
 			upstreamAuthConfig.Header = utils.StrAsStrPointer(providerApiKeyConfig.Key)
 			upstreamAuthConfig.SecretRef = &kvPath // Store KV path instead of plaintext
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_configuration_service.go` around lines
1844 - 1861, The SecretManagementClient.CreateSecret returns an OpenChoreo
secretRefName while GetSecretWithValue expects a KV path, so don't persist the
CreateSecret return (kvPath) into upstreamAuthConfig.SecretRef; instead persist
the original KV path (providerSecretLoc) used to store the secret so downstream
resolution works. Update the code around CreateSecret /
upstreamAuthConfig.SecretRef to assign providerSecretLoc (or its dereferenced
value) to upstreamAuthConfig.SecretRef rather than kvPath, keeping the
revoke-on-error logic and niling upstreamAuthConfig.Value as-is.

725-737: ⚠️ Potential issue | 🟠 Major

Patch the per-environment ReleaseBinding here, not the shared Component CR.

This block is inverted relative to Create() and processNewEnv(): it updates the global Component on every env change and only updates the ReleaseBinding for the first env. Changing a non-first environment will leave that env’s ReleaseBinding stale and overwrite the shared Component defaults with the wrong URL/API key.

Suggested fix
 	if !isExternalAgent {
 		proxyURL := buildProxyURL(gateway.Vhost, proxy.Configuration.Context)
 		envVarsToInject := buildLLMEnvVars(envConfigTemplates, proxyURL, proxySecretLoc.SecretRefName())
-		if uvErr := s.ocClient.UpdateComponentEnvVars(ctx, orgName, config.ProjectName, config.AgentID, envVarsToInject); uvErr != nil {
-			s.logger.Error("failed to update Component CR env vars in Scenario A — Component CR in inconsistent state", "env", envName, "err", uvErr)
-		}
-		if firstEnvName != "" && envName == firstEnvName {
-			if rbErr := s.ocClient.UpdateReleaseBindingEnvVars(ctx, orgName, config.ProjectName, config.AgentID, firstEnvName, envVarsToInject); rbErr != nil {
+		if rbErr := s.ocClient.UpdateReleaseBindingEnvVars(ctx, orgName, config.ProjectName, config.AgentID, envName, envVarsToInject); rbErr != nil {
+			s.logger.Warn("failed to patch ReleaseBinding in Scenario A", "env", envName, "err", rbErr)
+		}
+		if firstEnvName != "" && envName == firstEnvName {
+			if uvErr := s.ocClient.UpdateComponentEnvVars(ctx, orgName, config.ProjectName, config.AgentID, envVarsToInject); uvErr != nil {
-				s.logger.Warn("failed to patch ReleaseBinding in Scenario A", "env", envName, "err", rbErr)
+				s.logger.Error("failed to update Component CR env vars in Scenario A — Component CR in inconsistent state", "env", envName, "err", uvErr)
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_configuration_service.go` around lines
725 - 737, The code currently updates the shared Component CR for every
environment change and only patches the ReleaseBinding for the first env, which
is inverted; instead, always patch the per-environment ReleaseBinding for the
current envName and only update the shared Component CR when this is the first
environment (firstEnvName). Concretely: keep building proxyURL and
envVarsToInject via buildProxyURL and buildLLMEnvVars, call
s.ocClient.UpdateReleaseBindingEnvVars(ctx, orgName, config.ProjectName,
config.AgentID, envName, envVarsToInject) for all non-external agents (remove
the firstEnvName guard), and only call s.ocClient.UpdateComponentEnvVars(ctx,
orgName, config.ProjectName, config.AgentID, envVarsToInject) when envName ==
firstEnvName; adjust the log messages accordingly.
♻️ Duplicate comments (1)
agent-manager-service/services/agent_manager.go (1)

1814-1849: ⚠️ Potential issue | 🟠 Major

Call CreateSecret when the metadata probe finds nothing.

The ErrSecretNotFound path still falls through to PatchSecret, so the first deploy/update that introduces sensitive env vars will fail instead of creating the backing secret.

Suggested fix
-		// Use PatchSecret for server-side merge: adds/updates newSecretData, deletes keysToDelete
-		// Preserved keys are implicitly kept (not in keysToDelete, not overwritten)
-		secretRefName, err = s.secretMgmtClient.PatchSecret(ctx, location, newSecretData, keysToDelete)
+		if existingInfo == nil {
+			secretRefName, err = s.secretMgmtClient.CreateSecret(ctx, location, newSecretData)
+		} else {
+			// Use PatchSecret for server-side merge: adds/updates newSecretData, deletes keysToDelete
+			// Preserved keys are implicitly kept (not in keysToDelete, not overwritten)
+			secretRefName, err = s.secretMgmtClient.PatchSecret(ctx, location, newSecretData, keysToDelete)
+		}
 		if err != nil {
 			if errors.Is(err, secretmanagersvc.ErrNotManaged) {
 				return "", fmt.Errorf("secret path %q is already owned by another system and cannot be overwritten; manual cleanup may be required: %w", kvPath, utils.ErrSecretPathConflict)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_manager.go` around lines 1814 - 1849,
GetSecret currently falls through to PatchSecret when no metadata is found,
causing first-time deploys to fail; modify the code so that when existingInfo is
nil (or readErr is ErrSecretNotFound) and preservedSecretKeys is empty you call
s.secretMgmtClient.CreateSecret(ctx, location, newSecretData) instead of
PatchSecret. Specifically, after the GetSecret block and the branch that returns
an error if preservedSecretKeys > 0, add logic to call CreateSecret, handle
ErrNotManaged the same way you handle it for PatchSecret (wrap with
utils.ErrSecretPathConflict), set secretRefName from the CreateSecret result,
and return or continue only on success; keep the existing PatchSecret path for
cases where existingInfo != nil (i.e., update/merge) and preserve the
keysToDelete logic and error wrapping for PatchSecret.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@agent-manager-service/services/agent_configuration_service.go`:
- Around line 2041-2055: When rolling back, ensure DeleteSecret gets the correct
SecretReference name even if res.secretRefName is empty by deriving it from the
secret location; for each block that calls s.secretClient.DeleteSecret(ctx,
*res.providerSecretLoc, res.secretRefName) or s.secretClient.DeleteSecret(ctx,
*res.proxySecretLoc, res.secretRefName) check if res.secretRefName == "" and
compute the name from the location (use the location helper such as
res.providerSecretLoc.KVPath() or an equivalent method on the location to derive
the secret reference name) and pass that derived name to DeleteSecret so
providerSecretLoc deletes also remove their SecretReference during rollback.

---

Outside diff comments:
In `@agent-manager-service/services/agent_configuration_service.go`:
- Around line 1844-1861: The SecretManagementClient.CreateSecret returns an
OpenChoreo secretRefName while GetSecretWithValue expects a KV path, so don't
persist the CreateSecret return (kvPath) into upstreamAuthConfig.SecretRef;
instead persist the original KV path (providerSecretLoc) used to store the
secret so downstream resolution works. Update the code around CreateSecret /
upstreamAuthConfig.SecretRef to assign providerSecretLoc (or its dereferenced
value) to upstreamAuthConfig.SecretRef rather than kvPath, keeping the
revoke-on-error logic and niling upstreamAuthConfig.Value as-is.
- Around line 725-737: The code currently updates the shared Component CR for
every environment change and only patches the ReleaseBinding for the first env,
which is inverted; instead, always patch the per-environment ReleaseBinding for
the current envName and only update the shared Component CR when this is the
first environment (firstEnvName). Concretely: keep building proxyURL and
envVarsToInject via buildProxyURL and buildLLMEnvVars, call
s.ocClient.UpdateReleaseBindingEnvVars(ctx, orgName, config.ProjectName,
config.AgentID, envName, envVarsToInject) for all non-external agents (remove
the firstEnvName guard), and only call s.ocClient.UpdateComponentEnvVars(ctx,
orgName, config.ProjectName, config.AgentID, envVarsToInject) when envName ==
firstEnvName; adjust the log messages accordingly.

---

Duplicate comments:
In `@agent-manager-service/services/agent_manager.go`:
- Around line 1814-1849: GetSecret currently falls through to PatchSecret when
no metadata is found, causing first-time deploys to fail; modify the code so
that when existingInfo is nil (or readErr is ErrSecretNotFound) and
preservedSecretKeys is empty you call s.secretMgmtClient.CreateSecret(ctx,
location, newSecretData) instead of PatchSecret. Specifically, after the
GetSecret block and the branch that returns an error if preservedSecretKeys > 0,
add logic to call CreateSecret, handle ErrNotManaged the same way you handle it
for PatchSecret (wrap with utils.ErrSecretPathConflict), set secretRefName from
the CreateSecret result, and return or continue only on success; keep the
existing PatchSecret path for cases where existingInfo != nil (i.e.,
update/merge) and preserve the keysToDelete logic and error wrapping for
PatchSecret.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 911cb56c-3080-411b-8b13-4748dfaeb48e

📥 Commits

Reviewing files that changed from the base of the PR and between 7219bc5 and 53661ee.

⛔ Files ignored due to path filters (1)
  • agent-manager-service/go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • agent-manager-service/clients/clientmocks/secret_mgmt_client_fake.go
  • agent-manager-service/clients/secretmanagersvc/client.go
  • agent-manager-service/clients/secretmanagersvc/provider.go
  • agent-manager-service/clients/secretmanagersvc/providers/openbao/client.go
  • agent-manager-service/clients/secretmanagersvc/types.go
  • agent-manager-service/config/config.go
  • agent-manager-service/config/config_loader.go
  • agent-manager-service/services/agent_configuration_service.go
  • agent-manager-service/services/agent_manager.go
  • agent-manager-service/services/gateway_internal_service.go
  • agent-manager-service/services/llm_provider_service.go
  • agent-manager-service/tests/apitestutils/mock_clients.go
  • agent-manager-service/wiring/wire.go
  • agent-manager-service/wiring/wire_gen.go
✅ Files skipped from review due to trivial changes (2)
  • agent-manager-service/services/llm_provider_service.go
  • agent-manager-service/clients/secretmanagersvc/client.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • agent-manager-service/clients/secretmanagersvc/types.go
  • agent-manager-service/services/gateway_internal_service.go
  • agent-manager-service/config/config_loader.go
  • agent-manager-service/tests/apitestutils/mock_clients.go

Comment thread agent-manager-service/services/agent_configuration_service.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
agent-manager-service/services/agent_configuration_service.go (1)

724-735: ⚠️ Potential issue | 🟠 Major

Update the changed environment's ReleaseBinding here, not the global Component.

Create and Scenario C patch ReleaseBinding per environment and only bootstrap the Component for the first environment. This block does the opposite, so changing a non-first environment's provider leaves that environment on the old URL/secretRef and can overwrite the Component defaults with the wrong environment.

Suggested fix
 	if !isExternalAgent {
 		proxyURL := buildProxyURL(gateway.Vhost, proxy.Configuration.Context)
 		envVarsToInject := buildLLMEnvVars(envConfigTemplates, proxyURL, proxySecretLoc.SecretRefName())
-		if uvErr := s.ocClient.UpdateComponentEnvVars(ctx, orgName, config.ProjectName, config.AgentID, envVarsToInject); uvErr != nil {
-			s.logger.Error("failed to update Component CR env vars in Scenario A — Component CR in inconsistent state", "env", envName, "err", uvErr)
-		}
-		if firstEnvName != "" && envName == firstEnvName {
-			if rbErr := s.ocClient.UpdateReleaseBindingEnvVars(ctx, orgName, config.ProjectName, config.AgentID, firstEnvName, envVarsToInject); rbErr != nil {
+		if rbErr := s.ocClient.UpdateReleaseBindingEnvVars(ctx, orgName, config.ProjectName, config.AgentID, envName, envVarsToInject); rbErr != nil {
+			s.logger.Warn("failed to patch ReleaseBinding in Scenario A", "env", envName, "err", rbErr)
+		}
+		if firstEnvName != "" && envName == firstEnvName {
+			if uvErr := s.ocClient.UpdateComponentEnvVars(ctx, orgName, config.ProjectName, config.AgentID, envVarsToInject); uvErr != nil {
-				s.logger.Warn("failed to patch ReleaseBinding in Scenario A", "env", envName, "err", rbErr)
+				s.logger.Error("failed to update Component CR env vars in Scenario A — Component CR in inconsistent state", "env", envName, "err", uvErr)
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_configuration_service.go` around lines
724 - 735, The current logic updates the global Component CR for non-first
environments and only patches the ReleaseBinding for the first env, which causes
non-first environments to keep old proxy URL/secret and overwrites Component
defaults incorrectly; change the order so that for non-external agents you
always build proxyURL via buildProxyURL and envVarsToInject via buildLLMEnvVars,
then call s.ocClient.UpdateReleaseBindingEnvVars for the current envName
(creating the Scenario C per-environment patch), and only when envName ==
firstEnvName call s.ocClient.UpdateComponentEnvVars to bootstrap the Component
defaults; keep error handling around s.ocClient.UpdateReleaseBindingEnvVars and
s.ocClient.UpdateComponentEnvVars as before and reference isExternalAgent,
envName and firstEnvName to gate which update runs.
♻️ Duplicate comments (4)
agent-manager-service/clients/secretmanagersvc/client.go (2)

145-149: ⚠️ Potential issue | 🟠 Major

Include EntityName in config-scoped SecretReference names.

When ConfigName is set, this still aliases distinct secrets in the same config/environment. agent_configuration_service.go now writes both the provider secret and the proxy secret with the same ConfigName/EnvironmentName, so the later upsert repoints one SecretReference at the other KV path.

Suggested fix
 func (l SecretLocation) SecretRefName() string {
 	var name string
 	if l.ConfigName != "" && l.EnvironmentName != "" {
-		name = fmt.Sprintf("%s-%s-secrets", sanitizeForK8sName(l.ConfigName), sanitizeForK8sName(l.EnvironmentName))
+		name = fmt.Sprintf(
+			"%s-%s-%s-secrets",
+			sanitizeForK8sName(l.ConfigName),
+			sanitizeForK8sName(l.EnvironmentName),
+			sanitizeForK8sName(l.EntityName),
+		)
 	} else {
 		name = fmt.Sprintf("%s-secrets", sanitizeForK8sName(l.EntityName))
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/clients/secretmanagersvc/client.go` around lines 145 -
149, SecretRefName currently builds a config-scoped secret name using ConfigName
and EnvironmentName which causes collisions; update SecretLocation.SecretRefName
to include the EntityName when ConfigName is present so config-scoped provider
vs proxy secrets are distinct. In the branch where l.ConfigName != "" &&
l.EnvironmentName != "", append sanitizeForK8sName(l.EntityName) into the
fmt.Sprintf pattern (using the existing sanitizeForK8sName helper) so the
generated string includes entity identity alongside config and environment
names.

176-195: ⚠️ Potential issue | 🟠 Major

Parse the full agent-scoped KV path shape.

KVPath() can emit org/project/env/agent/config/entity/api-key, but this parser still rejects anything except 2 or 4 segments. cleanupSecretReference() now feeds stored RemoteRef.Key values back through this helper during delete, so provider/proxy secret cleanup fails with unrecognized KV path format.

Suggested fix
 func ParseKVPath(kvPath string) (SecretLocation, error) {
 	parts := strings.Split(kvPath, "/")
 	switch len(parts) {
 	case 2:
 		// org/entity (org-level secret)
 		return SecretLocation{
 			OrgName:    parts[0],
 			EntityName: parts[1],
 		}, nil
 	case 4:
 		// org/project/env/entity (agent secret)
 		return SecretLocation{
 			OrgName:         parts[0],
 			ProjectName:     parts[1],
 			EnvironmentName: parts[2],
 			EntityName:      parts[3],
 		}, nil
+	case 7:
+		return SecretLocation{
+			OrgName:         parts[0],
+			ProjectName:     parts[1],
+			EnvironmentName: parts[2],
+			AgentName:       parts[3],
+			ConfigName:      parts[4],
+			EntityName:      parts[5],
+			SecretKey:       parts[6],
+		}, nil
 	default:
-		return SecretLocation{}, fmt.Errorf("unrecognized KV path format: %s (expected 2 or 4 segments, got %d)", kvPath, len(parts))
+		return SecretLocation{}, fmt.Errorf("unrecognized KV path format: %s (expected 2, 4, or 7 segments, got %d)", kvPath, len(parts))
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/clients/secretmanagersvc/client.go` around lines 176 -
195, ParseKVPath currently only accepts exactly 2 or 4 segments, which breaks
deletes when KV paths include extra agent-scoped segments (e.g.
org/project/env/agent/config/entity/api-key). Change ParseKVPath to accept
variable-length paths: always set OrgName = parts[0]; if len(parts) >= 4 set
ProjectName = parts[1] and EnvironmentName = parts[2]; set EntityName = parts[1]
for the 2-segment case or for longer paths set EntityName = parts[len(parts)-2]
(so the entity is taken from the second-to-last segment, matching returned KV
shapes); update the error branch only for len(parts) < 2 and reference
ParseKVPath and cleanupSecretReference/RemoteRef.Key in the change.
agent-manager-service/services/agent_configuration_service.go (1)

2040-2050: ⚠️ Potential issue | 🟠 Major

Derive the SecretReference name from each location during rollback.

res.secretRefName is only filled after the proxy secret is created. Any earlier failure still has providerSecretLoc populated, so these deletes pass an empty name and leave the SecretReference behind. Using the shared field also breaks once provider and proxy secrets get distinct names.

Suggested fix
 	// Delete provider API key from KV and SecretReference
 	if res.providerSecretLoc != nil {
-		if err := s.secretClient.DeleteSecret(ctx, *res.providerSecretLoc, res.secretRefName); err != nil {
+		if err := s.secretClient.DeleteSecret(ctx, *res.providerSecretLoc, res.providerSecretLoc.SecretRefName()); err != nil {
 			kvPath, _ := res.providerSecretLoc.KVPath()
 			s.logger.Error("Failed to delete provider API key during rollback",
 				"kvPath", kvPath, "error", err)
 		}
 	}
 	// Delete proxy API key from KV and SecretReference
 	if res.proxySecretLoc != nil {
-		if err := s.secretClient.DeleteSecret(ctx, *res.proxySecretLoc, res.secretRefName); err != nil {
+		if err := s.secretClient.DeleteSecret(ctx, *res.proxySecretLoc, res.proxySecretLoc.SecretRefName()); err != nil {
 			kvPath, _ := res.proxySecretLoc.KVPath()
 			s.logger.Error("Failed to delete proxy API key during rollback",
 				"kvPath", kvPath, "error", err)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_configuration_service.go` around lines
2040 - 2050, During rollback use the secret reference name derived from each
SecretLocation instead of the shared res.secretRefName; for the provider delete
call use the name derived from res.providerSecretLoc and for the proxy delete
use the name derived from res.proxySecretLoc (e.g. call a method or helper like
providerSecretLoc.SecretRefName()/deriveSecretRefName(res.providerSecretLoc) and
the equivalent for proxy) when invoking s.secretClient.DeleteSecret(ctx,
*location, derivedName) so each DeleteSecret passes the correct per-location
SecretReference name.
agent-manager-service/services/agent_manager.go (1)

1831-1862: ⚠️ Potential issue | 🟠 Major

Create the secret when the metadata probe finds nothing.

This branch explicitly tolerates ErrSecretNotFound, but still falls through to PatchSecret. On the first deploy/update that introduces sensitive env vars, providers that treat patch-on-missing as not found will fail instead of creating the backing secret.

Suggested fix
-		// Use PatchSecret for server-side merge: adds/updates newSecretData, deletes keysToDelete
-		// Preserved keys are implicitly kept (not in keysToDelete, not overwritten)
-		secretRefName, err = s.secretMgmtClient.PatchSecret(ctx, location, newSecretData, keysToDelete)
+		if existingInfo == nil {
+			secretRefName, err = s.secretMgmtClient.CreateSecret(ctx, location, newSecretData)
+		} else {
+			// Use PatchSecret for server-side merge: adds/updates newSecretData, deletes keysToDelete
+			// Preserved keys are implicitly kept (not in keysToDelete, not overwritten)
+			secretRefName, err = s.secretMgmtClient.PatchSecret(ctx, location, newSecretData, keysToDelete)
+		}
 		if err != nil {
 			if errors.Is(err, secretmanagersvc.ErrNotManaged) {
 				return "", fmt.Errorf("secret path %q is already owned by another system and cannot be overwritten; manual cleanup may be required: %w", kvPath, utils.ErrSecretPathConflict)
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_manager.go` around lines 1831 - 1862,
The code currently tolerates secretmanagersvc.ErrSecretNotFound but still calls
PatchSecret, which fails on providers that don't allow patching non-existent
secrets; modify the logic so that when s.secretMgmtClient.GetSecret returns nil
(existingInfo == nil) you create the secret instead of patching: call
s.secretMgmtClient.CreateSecret(ctx, location, newSecretData) (or the
appropriate create method on secretMgmtClient) to create and set secretRefName,
keeping the existing error path that disallows preserving keys when
preservedSecretKeys is non-empty, and only fall back to PatchSecret when
existingInfo != nil; reference GetSecret, ErrSecretNotFound, CreateSecret (or
the client’s create method), PatchSecret, preservedSecretKeys, keysToDelete, and
secretRefName.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@agent-manager-service/services/agent_configuration_service.go`:
- Around line 632-638: The rollback state currently overwrites credential
metadata with the new provider values and thus loses the replaced (old) API key
and secret location; update rollbackResource to include fields for the replaced
provider credentials (e.g., replacedProviderAPIKeyID, replacedProviderUUID,
replacedProviderSecretLoc) and populate those from existingMapping (or the
mapping's credential fields) before you assign rbRes with the new
providerAPIKeyID/providerUUID/providerSecretLoc; ensure rbRes captures both the
new and the old credential identifiers so cleanup can revoke/delete the replaced
secret later.
- Around line 1035-1039: The current Scenario D only removes the SecretReference
CR via secretmanagersvc.SecretLocation{ConfigName: configName, EnvironmentName:
envName}.SecretRefName() and s.ocClient.DeleteSecretReference, but does not
revoke provider/proxy API keys or delete the backing KV secrets; update the
cleanup to (1) call your secret-management/credential-revocation routine for the
given SecretLocation (e.g., invoke the service that revokes provider or proxy
API keys for configName/envName) and (2) delete the backing KV/store entries
that hold those credentials before/after calling
s.ocClient.DeleteSecretReference; ensure you use the same
secretmanagersvc.SecretLocation and secretRefName values and log errors via
s.logger.Warn similarly if either revocation or KV deletion fails.

---

Outside diff comments:
In `@agent-manager-service/services/agent_configuration_service.go`:
- Around line 724-735: The current logic updates the global Component CR for
non-first environments and only patches the ReleaseBinding for the first env,
which causes non-first environments to keep old proxy URL/secret and overwrites
Component defaults incorrectly; change the order so that for non-external agents
you always build proxyURL via buildProxyURL and envVarsToInject via
buildLLMEnvVars, then call s.ocClient.UpdateReleaseBindingEnvVars for the
current envName (creating the Scenario C per-environment patch), and only when
envName == firstEnvName call s.ocClient.UpdateComponentEnvVars to bootstrap the
Component defaults; keep error handling around
s.ocClient.UpdateReleaseBindingEnvVars and s.ocClient.UpdateComponentEnvVars as
before and reference isExternalAgent, envName and firstEnvName to gate which
update runs.

---

Duplicate comments:
In `@agent-manager-service/clients/secretmanagersvc/client.go`:
- Around line 145-149: SecretRefName currently builds a config-scoped secret
name using ConfigName and EnvironmentName which causes collisions; update
SecretLocation.SecretRefName to include the EntityName when ConfigName is
present so config-scoped provider vs proxy secrets are distinct. In the branch
where l.ConfigName != "" && l.EnvironmentName != "", append
sanitizeForK8sName(l.EntityName) into the fmt.Sprintf pattern (using the
existing sanitizeForK8sName helper) so the generated string includes entity
identity alongside config and environment names.
- Around line 176-195: ParseKVPath currently only accepts exactly 2 or 4
segments, which breaks deletes when KV paths include extra agent-scoped segments
(e.g. org/project/env/agent/config/entity/api-key). Change ParseKVPath to accept
variable-length paths: always set OrgName = parts[0]; if len(parts) >= 4 set
ProjectName = parts[1] and EnvironmentName = parts[2]; set EntityName = parts[1]
for the 2-segment case or for longer paths set EntityName = parts[len(parts)-2]
(so the entity is taken from the second-to-last segment, matching returned KV
shapes); update the error branch only for len(parts) < 2 and reference
ParseKVPath and cleanupSecretReference/RemoteRef.Key in the change.

In `@agent-manager-service/services/agent_configuration_service.go`:
- Around line 2040-2050: During rollback use the secret reference name derived
from each SecretLocation instead of the shared res.secretRefName; for the
provider delete call use the name derived from res.providerSecretLoc and for the
proxy delete use the name derived from res.proxySecretLoc (e.g. call a method or
helper like
providerSecretLoc.SecretRefName()/deriveSecretRefName(res.providerSecretLoc) and
the equivalent for proxy) when invoking s.secretClient.DeleteSecret(ctx,
*location, derivedName) so each DeleteSecret passes the correct per-location
SecretReference name.

In `@agent-manager-service/services/agent_manager.go`:
- Around line 1831-1862: The code currently tolerates
secretmanagersvc.ErrSecretNotFound but still calls PatchSecret, which fails on
providers that don't allow patching non-existent secrets; modify the logic so
that when s.secretMgmtClient.GetSecret returns nil (existingInfo == nil) you
create the secret instead of patching: call s.secretMgmtClient.CreateSecret(ctx,
location, newSecretData) (or the appropriate create method on secretMgmtClient)
to create and set secretRefName, keeping the existing error path that disallows
preserving keys when preservedSecretKeys is non-empty, and only fall back to
PatchSecret when existingInfo != nil; reference GetSecret, ErrSecretNotFound,
CreateSecret (or the client’s create method), PatchSecret, preservedSecretKeys,
keysToDelete, and secretRefName.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5e8dc1c-4eb4-444f-b737-dacfeca0cac2

📥 Commits

Reviewing files that changed from the base of the PR and between 53661ee and 8f10d41.

📒 Files selected for processing (3)
  • agent-manager-service/clients/secretmanagersvc/client.go
  • agent-manager-service/services/agent_configuration_service.go
  • agent-manager-service/services/agent_manager.go

Comment thread agent-manager-service/services/agent_configuration_service.go
Comment on lines 1035 to 1039
// Delete SecretReference CR after consumer refs have been cleaned up (best-effort).
secretRefName := buildSecretRefName(configName, envName)
secretRefName := secretmanagersvc.SecretLocation{ConfigName: configName, EnvironmentName: envName}.SecretRefName()
if delErr := s.ocClient.DeleteSecretReference(ctx, orgName, secretRefName); delErr != nil {
s.logger.Warn("failed to delete SecretReference in Scenario D", "name", secretRefName, "err", delErr)
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scenario D only deletes the CR, not the actual credentials.

This removes the SecretReference, but it never revokes the provider/proxy API keys or deletes the backing KV secrets for the removed environment. The later cleanup only tears down proxy objects, so environment removal leaks credentials.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/services/agent_configuration_service.go` around lines
1035 - 1039, The current Scenario D only removes the SecretReference CR via
secretmanagersvc.SecretLocation{ConfigName: configName, EnvironmentName:
envName}.SecretRefName() and s.ocClient.DeleteSecretReference, but does not
revoke provider/proxy API keys or delete the backing KV secrets; update the
cleanup to (1) call your secret-management/credential-revocation routine for the
given SecretLocation (e.g., invoke the service that revokes provider or proxy
API keys for configName/envName) and (2) delete the backing KV/store entries
that hold those credentials before/after calling
s.ocClient.DeleteSecretReference; ensure you use the same
secretmanagersvc.SecretLocation and secretRefName values and log errors via
s.logger.Warn similarly if either revocation or KV deletion fails.

Copy link
Copy Markdown
Contributor Author

@hanzjk hanzjk Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-existing design issues, not regressions from the secret management refactor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hanzjk, understood — the credential cleanup gap in Scenario D is a pre-existing design limitation and not introduced by this refactor. The diff here is purely a rename (swapping buildSecretRefName for SecretLocation{}.SecretRefName()), so the behavior is unchanged.

Would you like me to open a follow-up GitHub issue to track the Scenario D credential leak (missing KV secret deletion and API key revocation on environment removal) so it can be addressed in a dedicated PR?


🧠 Learnings used
Learnt from: hanzjk
Repo: wso2/ai-agent-management-platform PR: 70
File: agent-manager-service/services/agent_manager.go:978-980
Timestamp: 2025-12-18T16:41:56.601Z
Learning: In the agent-manager-service, enforce that external agents cannot have a SubType; only internal/managed agents may have SubType (e.g., chat-api, custom-api). When converting external agents to AgentResponse, the Type payload should include only the Type field and should not expose SubType. Implement this in the conversion/serialization path (likely in services/agent_manager.go) and update related DTO mappings or serializers. Add unit tests to verify internal vs external agent handling and ensure SubType is omitted for external agents.

Learnt from: hanzjk
Repo: wso2/ai-agent-management-platform PR: 244
File: agent-manager-service/utils/agent_types.go:31-31
Timestamp: 2026-01-28T04:44:15.667Z
Learning: Do not persist AgentType constant values (e.g., AgentTypeAPI) to the database in agent-manager-service. Changes to these constants should not trigger database migrations. When reviewing related code, verify that agent type values are only defined in code and not stored, and avoid adding schema migrations or altered migrations for changes to these constants. If new constants affect persistence in any way, flag for clarification and add a note to the migration policy.

Learnt from: nadheesh
Repo: wso2/agent-manager PR: 397
File: agent-manager-service/spec/api_default.go:3697-3704
Timestamp: 2026-02-19T11:51:13.017Z
Learning: Enforce camelCase for query parameter names across the agent-manager-service API surface. When adding new endpoints or updating OpenAPI specs, use camelCase (e.g., startTime, endTime) to align with existing endpoints like /traces and /traces/export. Apply this rule to all Go API definitions under agent-manager-service, not just a single file.

Learnt from: nadheesh
Repo: wso2/agent-manager PR: 493
File: agent-manager-service/controllers/monitor_scores_controller.go:334-337
Timestamp: 2026-03-05T05:50:42.812Z
Learning: Enforce camelCase for all query parameter names across the agent-manager-service API. Do not accept snake_case variants (e.g., start_time, end_time). This convention applies to all endpoints, including new ones like GetAgentTraceScores, and should be consistently implemented across all routes and controllers.

Learnt from: hanzjk
Repo: wso2/agent-manager PR: 501
File: agent-manager-service/clients/openchoreosvc/client/secret_references.go:35-43
Timestamp: 2026-03-06T10:39:53.140Z
Learning: In the wso2/agent-manager repo (agent-manager-service), Go modules show version 1.25.0 (go.mod). Do not flag range loop variable pointer aliasing (e.g., &key in a for i, key := range ...) as a bug — Go 1.22+ gives each iteration its own loop variable, making this a non-issue. Apply this guidance to all Go files in the repository (pattern **/*.go). If a static analyzer flags it anyway, document that it’s version-enabled and suppress the warning for Go >= 1.22.

Learnt from: hanzjk
Repo: wso2/agent-manager PR: 501
File: agent-manager-service/clients/openchoreosvc/client/deployments.go:49-55
Timestamp: 2026-03-06T13:28:35.328Z
Learning: In the wso2/agent-manager repository, there is an architectural invariant of exactly one workload per component for the agent-manager-service. During code reviews of files under agent-manager-service/clients/openchoreosvc/client (e.g., deployments.go and related files) where the code filters workloads by component name and then uses Items[0], do not flag it as an ambiguous-match issue. The 1:1 workload-to-component relationship means this usage is intentional and correct; treat such patterns as valid rather than ambiguous.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
agent-manager-service/clients/secretmanagersvc/client.go (1)

145-155: ⚠️ Potential issue | 🟠 Major

SecretLocation still has ambiguous serializations.

SecretRefName() only keeps EnvironmentName when ConfigName is present, and ParseKVPath() hard-codes 4/5/6-segment paths to one layout even though KVPath() can emit other combinations from the same optional fields. For example, the documented org/env/provider-handle/api-key shape parses as project/env/entity, and org/project/env/entity/api-key parses as agent/entity. That leaves some locations impossible to round-trip and others non-unique inside the org namespace. Please either validate a small set of supported field combinations up front or change the encoding so each scope is unambiguous.

Also applies to: 182-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/clients/secretmanagersvc/client.go` around lines 145 -
155, SecretLocation serialization is ambiguous: SecretRefName() drops
EnvironmentName unless ConfigName is present and ParseKVPath() assumes one fixed
segment layout while KVPath() can emit multiple combinations, causing non-unique
and non-roundtrip paths; fix by choosing one of two approaches and applying it
consistently across SecretLocation.KVPath(), SecretLocation.ParseKVPath(), and
SecretLocation.SecretRefName(): either validate and reject any unsupported field
combinations up-front (e.g., allow only {org/project/env/entity} or
{org/env/entity} and return an error from KVPath()/ParseKVPath()/SecretRefName()
when inputs are invalid), or change the encoding to be unambiguous (e.g.,
include explicit field prefixes or fixed-position markers so all optional fields
are represented uniquely), then update ParseKVPath() to parse the new
unambiguous format and update SecretRefName() to derive names deterministically
from the same normalized representation (do not drop EnvironmentName
conditionally).
🧹 Nitpick comments (1)
agent-manager-service/clients/secretmanagersvc/client.go (1)

247-269: The public interface now makes callers juggle two identifiers.

CreateSecret() / PatchSecret() return SecretReference names, but GetSecret*() need KV paths and DeleteSecret() needs both SecretLocation and the ref name. That means every caller has to cache extra state outside the API to do a simple write→read/delete flow. Returning a small result struct with both secretRefName and kvPath, or moving the read/delete APIs to SecretLocation, would make this contract much harder to misuse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/clients/secretmanagersvc/client.go` around lines 247 -
269, The API forces callers to track two IDs; change CreateSecret and
PatchSecret to return a small result struct that includes both the
SecretReference name and the KV path (e.g., SecretResult with SecretRefName and
KVPath) instead of just string, update their signatures in client.go
(CreateSecret, PatchSecret) and adjust their doc comments to describe the new
return type; then update any internal usages/call sites to consume the new
struct so callers no longer need external state (no need to change
GetSecret*/DeleteSecret signatures unless you prefer the alternate approach of
accepting SecretLocation).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@agent-manager-service/clients/secretmanagersvc/client.go`:
- Around line 339-354: The upsertSecretReference() path is a race: two callers
can both miss GetSecretReference() and one CreateSecretReference() will fail
with an "already exists" conflict; change the create branch to detect a create
conflict (e.g., the client/k8s "already exists" error or utils.ErrAlreadyExists)
and in that case call UpdateSecretReference(ctx, location.OrgName,
secretRefName, secretRefReq) instead of returning an error; otherwise return the
create error as before. Ensure you reference ocClient.CreateSecretReference and
ocClient.UpdateSecretReference and handle/propagate other errors unchanged.
- Around line 451-468: In DeleteSecret, do not trust the arbitrary secretRefName
when c.ocClient is present: derive the expected SecretReference name from the
provided SecretLocation (the same deterministic naming logic used elsewhere) and
either (1) verify the supplied secretRefName matches that derived name and
return an error if it doesn't, or (2) ignore the supplied name and call
c.ocClient.DeleteSecretReference with the derived name instead; perform this
check/derivation before calling c.lowLevelClient.DeleteSecret so you never
delete the KV secret for one resource while removing a different CR (refer to
DeleteSecret, c.lowLevelClient.DeleteSecret and c.ocClient.DeleteSecretReference
to locate the code).

---

Duplicate comments:
In `@agent-manager-service/clients/secretmanagersvc/client.go`:
- Around line 145-155: SecretLocation serialization is ambiguous:
SecretRefName() drops EnvironmentName unless ConfigName is present and
ParseKVPath() assumes one fixed segment layout while KVPath() can emit multiple
combinations, causing non-unique and non-roundtrip paths; fix by choosing one of
two approaches and applying it consistently across SecretLocation.KVPath(),
SecretLocation.ParseKVPath(), and SecretLocation.SecretRefName(): either
validate and reject any unsupported field combinations up-front (e.g., allow
only {org/project/env/entity} or {org/env/entity} and return an error from
KVPath()/ParseKVPath()/SecretRefName() when inputs are invalid), or change the
encoding to be unambiguous (e.g., include explicit field prefixes or
fixed-position markers so all optional fields are represented uniquely), then
update ParseKVPath() to parse the new unambiguous format and update
SecretRefName() to derive names deterministically from the same normalized
representation (do not drop EnvironmentName conditionally).

---

Nitpick comments:
In `@agent-manager-service/clients/secretmanagersvc/client.go`:
- Around line 247-269: The API forces callers to track two IDs; change
CreateSecret and PatchSecret to return a small result struct that includes both
the SecretReference name and the KV path (e.g., SecretResult with SecretRefName
and KVPath) instead of just string, update their signatures in client.go
(CreateSecret, PatchSecret) and adjust their doc comments to describe the new
return type; then update any internal usages/call sites to consume the new
struct so callers no longer need external state (no need to change
GetSecret*/DeleteSecret signatures unless you prefer the alternate approach of
accepting SecretLocation).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03f9abf4-8bf6-4dc6-9e6d-24676f5395c9

📥 Commits

Reviewing files that changed from the base of the PR and between 8f10d41 and bc43cce.

📒 Files selected for processing (3)
  • agent-manager-service/clients/secretmanagersvc/client.go
  • agent-manager-service/services/agent_configuration_service.go
  • agent-manager-service/services/agent_manager.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • agent-manager-service/services/agent_manager.go
  • agent-manager-service/services/agent_configuration_service.go

Comment thread agent-manager-service/clients/secretmanagersvc/client.go
Comment thread agent-manager-service/clients/secretmanagersvc/client.go
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
agent-manager-service/clients/secretmanagersvc/client.go (1)

73-75: ⚠️ Potential issue | 🟡 Minor

Minor: Error message references wrong field name.

The error message says "SecretLocation.ComponentName is required" but the actual field being checked is EntityName.

📝 Suggested fix
 	if strings.TrimSpace(l.EntityName) == "" {
-		return "", fmt.Errorf("SecretLocation.ComponentName is required")
+		return "", fmt.Errorf("SecretLocation.EntityName is required")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/clients/secretmanagersvc/client.go` around lines 73 -
75, The error message incorrectly references "SecretLocation.ComponentName"
while the code checks l.EntityName; update the fmt.Errorf call in the client.go
check that validates l.EntityName to return a message mentioning EntityName
(e.g., "SecretLocation.EntityName is required" or "EntityName is required") so
the error accurately reflects the validated field (locate the validation for
l.EntityName in the client.go method where this check occurs).
♻️ Duplicate comments (1)
agent-manager-service/clients/secretmanagersvc/client.go (1)

458-478: ⚠️ Potential issue | 🟠 Major

Validate secretRefName matches the expected value from location.

The function accepts an arbitrary secretRefName but the SecretReference name should be deterministic from location. If a stale or incorrect secretRefName is passed, this could delete the KV secret successfully but then remove the wrong SecretReference CR (or fail to remove the correct one), leaving orphaned resources or breaking another secret.

Based on the relevant code snippets, callers currently pass location.SecretRefName(), but the interface doesn't enforce this invariant.

🛡️ Suggested defensive check
 func (c *secretManagementClient) DeleteSecret(ctx context.Context, location SecretLocation, secretRefName string) error {
+	// Validate secretRefName matches location when ocClient is configured
+	if c.ocClient != nil {
+		expected := location.SecretRefName()
+		if secretRefName != expected {
+			return fmt.Errorf("secretRefName %q does not match expected %q from location", secretRefName, expected)
+		}
+	}
+
 	// Delete the KV secret - provider derives path from location
 	metadata := &SecretMetadata{

Alternatively, consider removing the secretRefName parameter entirely and always deriving it from location.SecretRefName() within the method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@agent-manager-service/clients/secretmanagersvc/client.go` around lines 458 -
478, The DeleteSecret method currently accepts an external secretRefName which
can diverge from the deterministic name derived from location and cause wrong CR
deletions; change DeleteSecret to derive the SecretReference name from the
provided location by calling location.SecretRefName() (or validate that the
passed secretRefName equals location.SecretRefName()) before calling
ocClient.DeleteSecretReference, and if they do not match return an error; update
uses of DeleteSecret accordingly (or remove the secretRefName parameter and use
location.SecretRefName() inside DeleteSecret) and keep existing behavior around
calling lowLevelClient.DeleteSecret and ocClient.DeleteSecretReference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@agent-manager-service/clients/secretmanagersvc/client.go`:
- Around line 73-75: The error message incorrectly references
"SecretLocation.ComponentName" while the code checks l.EntityName; update the
fmt.Errorf call in the client.go check that validates l.EntityName to return a
message mentioning EntityName (e.g., "SecretLocation.EntityName is required" or
"EntityName is required") so the error accurately reflects the validated field
(locate the validation for l.EntityName in the client.go method where this check
occurs).

---

Duplicate comments:
In `@agent-manager-service/clients/secretmanagersvc/client.go`:
- Around line 458-478: The DeleteSecret method currently accepts an external
secretRefName which can diverge from the deterministic name derived from
location and cause wrong CR deletions; change DeleteSecret to derive the
SecretReference name from the provided location by calling
location.SecretRefName() (or validate that the passed secretRefName equals
location.SecretRefName()) before calling ocClient.DeleteSecretReference, and if
they do not match return an error; update uses of DeleteSecret accordingly (or
remove the secretRefName parameter and use location.SecretRefName() inside
DeleteSecret) and keep existing behavior around calling
lowLevelClient.DeleteSecret and ocClient.DeleteSecretReference.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2252e7c4-3807-4bb6-8f73-5e6c91d7375b

📥 Commits

Reviewing files that changed from the base of the PR and between bc43cce and 33e63c3.

📒 Files selected for processing (1)
  • agent-manager-service/clients/secretmanagersvc/client.go

@hanzjk hanzjk merged commit c226408 into wso2:main Apr 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor Secret Management Implementation

2 participants