Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/skills/ambient-pr-test/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,14 @@ docker login quay.io

Either `build.sh` was not run or the CI build workflow failed. Check Actions → `Build and Push Component Docker Images` for the PR.

### Runner pods can't reach external hosts (Squid proxy)

The MPP cluster routes outbound traffic through a Squid proxy (`proxy.squi-001.prod.iad2.dc.redhat.com:3128`). The `runtime-int` deployments have `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` set in their pod specs, but runner pods spawned by the control plane did not inherit these.

**Fix (merged):** The control plane reads `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` from its own environment and injects them into both the runner container (`buildEnv()`) and the MCP sidecar container (`buildMCPSidecar()`). No manifest change needed — the CP's deployment already has the proxy vars; they now propagate automatically.

**Pattern:** When the CP needs to forward platform-level env vars to spawned pods, add the field to `ControlPlaneConfig` → `KubeReconcilerConfig` → `buildEnv()`/`buildMCPSidecar()`.

### JWT / UNAUTHENTICATED errors in api-server

The production overlay configures JWT against Red Hat SSO. For ephemeral test instances without SSO integration:
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/devflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Then re-read this file before continuing.

## Workflow Overview

```
```text
1. Ticket (optional)
2. Branch (from ticket name if available)
3. Spec change (load + modify the component's .spec.md)
Expand Down
27 changes: 19 additions & 8 deletions components/ambient-api-server/plugins/agents/start_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,27 @@ func NewStartHandler(agent AgentService, inboxSvc inbox.InboxMessageService, ses
}

func (h *startHandler) Start(w http.ResponseWriter, r *http.Request) {
cfg := &handlers.HandlerConfig{
Action: func() (interface{}, *pkgerrors.ServiceError) {
ctx := r.Context()
agentID := mux.Vars(r)["agent_id"]
ctx := r.Context()
agentID := mux.Vars(r)["agent_id"]

agent, err := h.agent.Get(ctx, agentID)
if err != nil {
return nil, err
}
agent, err := h.agent.Get(ctx, agentID)
if err != nil {
handlers.HandleError(ctx, w, err)
return
}

if existing, _ := h.session.ActiveByAgentID(ctx, agentID); existing != nil {
resp := &StartResponse{
Session: sessions.PresentSession(existing),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)
return
}
Comment on lines +59 to +67
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.

⚠️ Potential issue | 🟠 Major

Handler should propagate errors from ActiveByAgentID check.

Currently discards the error with _. If the service is updated to return real errors (per comment on service.go), this should be:

existing, svcErr := h.session.ActiveByAgentID(ctx, agentID)
if svcErr != nil {
    handlers.HandleError(ctx, w, svcErr)
    return
}
if existing != nil {
    // return 200 with existing session
}

This ensures idempotency isn't silently bypassed during DB issues.

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

In `@components/ambient-api-server/plugins/agents/start_handler.go` around lines
59 - 67, The handler currently ignores errors from h.session.ActiveByAgentID;
update the call to capture the error (e.g., existing, svcErr :=
h.session.ActiveByAgentID(ctx, agentID)), check if svcErr != nil and call
handlers.HandleError(ctx, w, svcErr) then return, and only proceed to build
StartResponse and write the 200 JSON when existing != nil; keep using
StartResponse and sessions.PresentSession for the successful path.


cfg := &handlers.HandlerConfig{
Action: func() (interface{}, *pkgerrors.ServiceError) {
unread, inboxErr := h.inbox.UnreadByAgentID(ctx, agentID)
if inboxErr != nil {
return nil, inboxErr
Expand Down
28 changes: 27 additions & 1 deletion components/ambient-api-server/plugins/credentials/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package credentials
import (
"fmt"
"net/http"
"regexp"

"github.com/gorilla/mux"

Expand All @@ -13,6 +14,8 @@ import (
"github.com/openshift-online/rh-trex-ai/pkg/services"
)

var safeProjectIDPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)

var _ handlers.RestHandler = credentialHandler{}

type credentialHandler struct {
Expand Down Expand Up @@ -58,11 +61,15 @@ func (h credentialHandler) Patch(w http.ResponseWriter, r *http.Request) {
Validators: []handlers.Validate{},
Action: func() (interface{}, *errors.ServiceError) {
ctx := r.Context()
projectID := mux.Vars(r)["id"]
id := mux.Vars(r)["cred_id"]
found, err := h.credential.Get(ctx, id)
if err != nil {
return nil, err
}
if found.ProjectID != projectID {
return nil, errors.NotFound("credential with id='%s' not found", id)
}

if patch.Name != nil {
found.Name = *patch.Name
Expand Down Expand Up @@ -106,6 +113,9 @@ func (h credentialHandler) List(w http.ResponseWriter, r *http.Request) {
Action: func() (interface{}, *errors.ServiceError) {
ctx := r.Context()
projectID := mux.Vars(r)["id"]
if !safeProjectIDPattern.MatchString(projectID) {
return nil, errors.Validation("invalid project ID format")
}

listArgs := services.NewListArguments(r.URL.Query())
projectFilter := fmt.Sprintf("project_id = '%s'", projectID)
Expand Down Expand Up @@ -148,12 +158,16 @@ func (h credentialHandler) List(w http.ResponseWriter, r *http.Request) {
func (h credentialHandler) Get(w http.ResponseWriter, r *http.Request) {
cfg := &handlers.HandlerConfig{
Action: func() (interface{}, *errors.ServiceError) {
projectID := mux.Vars(r)["id"]
id := mux.Vars(r)["cred_id"]
ctx := r.Context()
credential, err := h.credential.Get(ctx, id)
if err != nil {
return nil, err
}
if credential.ProjectID != projectID {
return nil, errors.NotFound("credential with id='%s' not found", id)
}

return PresentCredential(credential), nil
},
Expand All @@ -165,9 +179,17 @@ func (h credentialHandler) Get(w http.ResponseWriter, r *http.Request) {
func (h credentialHandler) Delete(w http.ResponseWriter, r *http.Request) {
cfg := &handlers.HandlerConfig{
Action: func() (interface{}, *errors.ServiceError) {
projectID := mux.Vars(r)["id"]
id := mux.Vars(r)["cred_id"]
ctx := r.Context()
err := h.credential.Delete(ctx, id)
found, err := h.credential.Get(ctx, id)
if err != nil {
return nil, err
}
if found.ProjectID != projectID {
return nil, errors.NotFound("credential with id='%s' not found", id)
}
err = h.credential.Delete(ctx, id)
if err != nil {
return nil, err
}
Expand All @@ -180,12 +202,16 @@ func (h credentialHandler) Delete(w http.ResponseWriter, r *http.Request) {
func (h credentialHandler) GetToken(w http.ResponseWriter, r *http.Request) {
cfg := &handlers.HandlerConfig{
Action: func() (interface{}, *errors.ServiceError) {
projectID := mux.Vars(r)["id"]
id := mux.Vars(r)["cred_id"]
ctx := r.Context()
credential, err := h.credential.Get(ctx, id)
if err != nil {
return nil, err
}
if credential.ProjectID != projectID {
return nil, errors.NotFound("credential with id='%s' not found", id)
}

return PresentCredentialToken(credential), nil
},
Expand Down
13 changes: 13 additions & 0 deletions components/ambient-api-server/plugins/sessions/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type SessionDao interface {
FindByIDs(ctx context.Context, ids []string) (SessionList, error)
All(ctx context.Context) (SessionList, error)
AllByProjectId(ctx context.Context, projectId string) (SessionList, error)
ActiveByAgentID(ctx context.Context, agentID string) (*Session, error)
}

var _ SessionDao = &sqlSessionDao{}
Expand Down Expand Up @@ -91,3 +92,15 @@ func (d *sqlSessionDao) AllByProjectId(ctx context.Context, projectId string) (S
}
return sessions, nil
}

func (d *sqlSessionDao) ActiveByAgentID(ctx context.Context, agentID string) (*Session, error) {
g2 := (*d.sessionFactory).New(ctx)
var session Session
err := g2.Where("agent_id = ? AND phase IN (?)", agentID, []string{"Pending", "Creating", "Running"}).
Order("created_at DESC").
Take(&session).Error
if err != nil {
return nil, err
}
return &session, nil
}
10 changes: 10 additions & 0 deletions components/ambient-api-server/plugins/sessions/mock_dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ func (d *sessionDaoMock) AllByProjectId(ctx context.Context, projectId string) (
}
return filtered, nil
}

func (d *sessionDaoMock) ActiveByAgentID(ctx context.Context, agentID string) (*Session, error) {
activePhases := map[string]bool{"Pending": true, "Creating": true, "Running": true}
for _, s := range d.sessions {
if s.AgentId != nil && *s.AgentId == agentID && s.Phase != nil && activePhases[*s.Phase] {
return s, nil
}
}
return nil, gorm.ErrRecordNotFound
}
9 changes: 9 additions & 0 deletions components/ambient-api-server/plugins/sessions/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type SessionService interface {
UpdateStatus(ctx context.Context, id string, patch *SessionStatusPatchRequest) (*Session, *errors.ServiceError)
Start(ctx context.Context, id string) (*Session, *errors.ServiceError)
Stop(ctx context.Context, id string) (*Session, *errors.ServiceError)
ActiveByAgentID(ctx context.Context, agentID string) (*Session, *errors.ServiceError)

FindByIDs(ctx context.Context, ids []string) (SessionList, *errors.ServiceError)

Expand Down Expand Up @@ -265,6 +266,14 @@ func (s *sqlSessionService) Start(ctx context.Context, id string) (*Session, *er
return session, nil
}

func (s *sqlSessionService) ActiveByAgentID(ctx context.Context, agentID string) (*Session, *errors.ServiceError) {
session, err := s.sessionDao.ActiveByAgentID(ctx, agentID)
if err != nil {
return nil, nil
}
return session, nil
}
Comment on lines +269 to +275
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.

⚠️ Potential issue | 🟠 Major

Swallowing all errors breaks idempotency under DB instability.

Returning (nil, nil) for any DAO error treats DB connection failures the same as "no active session found." Combined with the handler discarding the error (start_handler.go:59), a transient DB failure during the check could lead to duplicate session creation if the DB recovers before Create().

Consider distinguishing gorm.ErrRecordNotFound from other errors:

Proposed fix
 func (s *sqlSessionService) ActiveByAgentID(ctx context.Context, agentID string) (*Session, *errors.ServiceError) {
 	session, err := s.sessionDao.ActiveByAgentID(ctx, agentID)
 	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil, nil
+		}
+		return nil, errors.GeneralError("failed to check active session: %v", err)
-		return nil, nil
 	}
 	return session, nil
 }

Then update the handler to fail fast on real errors instead of proceeding to create.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *sqlSessionService) ActiveByAgentID(ctx context.Context, agentID string) (*Session, *errors.ServiceError) {
session, err := s.sessionDao.ActiveByAgentID(ctx, agentID)
if err != nil {
return nil, nil
}
return session, nil
}
func (s *sqlSessionService) ActiveByAgentID(ctx context.Context, agentID string) (*Session, *errors.ServiceError) {
session, err := s.sessionDao.ActiveByAgentID(ctx, agentID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, errors.GeneralError("failed to check active session: %v", err)
}
return session, nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-api-server/plugins/sessions/service.go` around lines 269 -
275, The ActiveByAgentID method in sqlSessionService currently swallows all DAO
errors by returning (nil, nil); instead, check the error from
sessionDao.ActiveByAgentID using errors.Is(err, gorm.ErrRecordNotFound) and only
treat ErrRecordNotFound as a nil result, otherwise wrap/convert the real DB
error into a non-nil *errors.ServiceError and return it (do not return nil, nil
on real errors). Update the caller (the start handler that invokes
ActiveByAgentID) to fail fast when a non-nil ServiceError is returned rather
than proceeding to Create, so transient DB failures do not lead to duplicate
sessions.


func (s *sqlSessionService) Stop(ctx context.Context, id string) (*Session, *errors.ServiceError) {
session, err := s.sessionDao.Get(ctx, id)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ func runKubeMode(ctx context.Context, cfg *config.ControlPlaneConfig) error {
CPRuntimeNamespace: cfg.CPRuntimeNamespace,
CPTokenURL: cfg.CPTokenURL,
CPTokenPublicKey: string(kp.PublicKeyPEM),
HTTPProxy: cfg.HTTPProxy,
HTTPSProxy: cfg.HTTPSProxy,
NoProxy: cfg.NoProxy,
}

conn, err := grpc.NewClient(cfg.GRPCServerAddr, grpc.WithTransportCredentials(grpcCredentials(cfg.GRPCUseTLS)))
Expand Down
6 changes: 6 additions & 0 deletions components/ambient-control-plane/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type ControlPlaneConfig struct {
ProjectKubeTokenFile string
CPTokenListenAddr string
CPTokenURL string
HTTPProxy string
HTTPSProxy string
NoProxy string
}

func Load() (*ControlPlaneConfig, error) {
Expand Down Expand Up @@ -75,6 +78,9 @@ func Load() (*ControlPlaneConfig, error) {
ProjectKubeTokenFile: os.Getenv("PROJECT_KUBE_TOKEN_FILE"),
CPTokenListenAddr: envOrDefault("CP_TOKEN_LISTEN_ADDR", ":8080"),
CPTokenURL: os.Getenv("CP_TOKEN_URL"),
HTTPProxy: os.Getenv("HTTP_PROXY"),
HTTPSProxy: os.Getenv("HTTPS_PROXY"),
NoProxy: os.Getenv("NO_PROXY"),
}

if cfg.MCPAPIServerURL == "" {
Expand Down
40 changes: 34 additions & 6 deletions components/ambient-control-plane/internal/kubeclient/kubeclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/rs/zerolog"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -201,7 +202,7 @@ func (kc *KubeClient) DeletePod(ctx context.Context, namespace, name string, opt
}

func (kc *KubeClient) DeletePodsByLabel(ctx context.Context, namespace, labelSelector string) error {
return kc.dynamic.Resource(PodGVR).Namespace(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labelSelector})
return kc.deleteCollectionWithFallback(ctx, PodGVR, namespace, labelSelector)
}

// Service operations
Expand All @@ -214,7 +215,7 @@ func (kc *KubeClient) CreateService(ctx context.Context, obj *unstructured.Unstr
}

func (kc *KubeClient) DeleteServicesByLabel(ctx context.Context, namespace, labelSelector string) error {
return kc.dynamic.Resource(ServiceGVR).Namespace(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labelSelector})
return kc.deleteCollectionWithFallback(ctx, ServiceGVR, namespace, labelSelector)
}

// Secret operations
Expand All @@ -231,7 +232,7 @@ func (kc *KubeClient) UpdateSecret(ctx context.Context, obj *unstructured.Unstru
}

func (kc *KubeClient) DeleteSecretsByLabel(ctx context.Context, namespace, labelSelector string) error {
return kc.dynamic.Resource(SecretGVR).Namespace(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labelSelector})
return kc.deleteCollectionWithFallback(ctx, SecretGVR, namespace, labelSelector)
}

// ServiceAccount operations
Expand All @@ -244,7 +245,7 @@ func (kc *KubeClient) CreateServiceAccount(ctx context.Context, obj *unstructure
}

func (kc *KubeClient) DeleteServiceAccountsByLabel(ctx context.Context, namespace, labelSelector string) error {
return kc.dynamic.Resource(ServiceAccountGVR).Namespace(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labelSelector})
return kc.deleteCollectionWithFallback(ctx, ServiceAccountGVR, namespace, labelSelector)
}

// Role operations
Expand All @@ -257,11 +258,38 @@ func (kc *KubeClient) CreateRole(ctx context.Context, obj *unstructured.Unstruct
}

func (kc *KubeClient) DeleteRolesByLabel(ctx context.Context, namespace, labelSelector string) error {
return kc.dynamic.Resource(RoleGVR).Namespace(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labelSelector})
return kc.deleteCollectionWithFallback(ctx, RoleGVR, namespace, labelSelector)
}

func (kc *KubeClient) DeleteRoleBindingsByLabel(ctx context.Context, namespace, labelSelector string) error {
return kc.dynamic.Resource(RoleBindingGVR).Namespace(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labelSelector})
return kc.deleteCollectionWithFallback(ctx, RoleBindingGVR, namespace, labelSelector)
}

func (kc *KubeClient) deleteCollectionWithFallback(ctx context.Context, gvr schema.GroupVersionResource, namespace, labelSelector string) error {
err := kc.dynamic.Resource(gvr).Namespace(namespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{LabelSelector: labelSelector})
if err == nil {
return nil
}
if !k8serrors.IsForbidden(err) {
return err
}

kc.logger.Warn().Str("resource", gvr.Resource).Str("namespace", namespace).Msg("deletecollection forbidden, falling back to list+delete")

list, listErr := kc.dynamic.Resource(gvr).Namespace(namespace).List(ctx, metav1.ListOptions{LabelSelector: labelSelector})
if listErr != nil {
return fmt.Errorf("fallback list %s: %w", gvr.Resource, listErr)
}

var lastErr error
for i := range list.Items {
name := list.Items[i].GetName()
if delErr := kc.dynamic.Resource(gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}); delErr != nil && !k8serrors.IsNotFound(delErr) {
kc.logger.Warn().Err(delErr).Str("resource", gvr.Resource).Str("name", name).Msg("fallback delete failed")
lastErr = delErr
}
}
return lastErr
}

func (kc *KubeClient) GetNetworkPolicy(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type KubeReconcilerConfig struct {
CPRuntimeNamespace string
CPTokenURL string
CPTokenPublicKey string
HTTPProxy string
HTTPSProxy string
NoProxy string
}

type SimpleKubeReconciler struct {
Expand Down Expand Up @@ -441,8 +444,12 @@ func (r *SimpleKubeReconciler) ensurePod(ctx context.Context, namespace string,
}

if useMCPSidecar {
containers = append(containers, r.buildMCPSidecar(session.ID))
r.logger.Info().Str("session_id", session.ID).Msg("MCP sidecar enabled for session")
if r.cfg.CPTokenURL == "" || r.cfg.CPTokenPublicKey == "" {
r.logger.Warn().Str("session_id", session.ID).Msg("MCP sidecar skipped: CP_TOKEN_URL or CPTokenPublicKey not configured")
} else {
containers = append(containers, r.buildMCPSidecar(session.ID))
r.logger.Info().Str("session_id", session.ID).Msg("MCP sidecar enabled for session")
}
Comment on lines +447 to +452
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.

⚠️ Potential issue | 🟠 Major

Don’t advertise MCP when the sidecar is skipped.

Lines 447-452 can omit the MCP container, but useMCPSidecar was already computed from MCPImage alone at Line 411 and passed into buildEnv at Line 426. That still sets AMBIENT_MCP_URL (Lines 607-609), so the runner will target localhost:8090 even though nothing is listening there.

Suggested fix
-	useMCPSidecar := r.cfg.MCPImage != ""
+	useMCPSidecar := r.cfg.MCPImage != "" && r.cfg.CPTokenURL != "" && r.cfg.CPTokenPublicKey != ""

 	containers := []interface{}{
 		map[string]interface{}{
 			"name":            "ambient-code-runner",
@@
-	if useMCPSidecar {
-		if r.cfg.CPTokenURL == "" || r.cfg.CPTokenPublicKey == "" {
-			r.logger.Warn().Str("session_id", session.ID).Msg("MCP sidecar skipped: CP_TOKEN_URL or CPTokenPublicKey not configured")
-		} else {
-			containers = append(containers, r.buildMCPSidecar(session.ID))
-			r.logger.Info().Str("session_id", session.ID).Msg("MCP sidecar enabled for session")
-		}
+	if r.cfg.MCPImage != "" && !useMCPSidecar {
+		r.logger.Warn().Str("session_id", session.ID).Msg("MCP sidecar skipped: CP_TOKEN_URL or CPTokenPublicKey not configured")
+	}
+	if useMCPSidecar {
+		containers = append(containers, r.buildMCPSidecar(session.ID))
+		r.logger.Info().Str("session_id", session.ID).Msg("MCP sidecar enabled for session")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-control-plane/internal/reconciler/kube_reconciler.go`
around lines 447 - 452, The code currently computes useMCPSidecar from MCPImage
only but still passes it into buildEnv causing AMBIENT_MCP_URL to be set even
when the CP token config is missing; change the logic to compute an enabled flag
(e.g., enabledMCPSidecar or enableMCPSidecar) that is true only when
useMCPSidecar is true AND r.cfg.CPTokenURL and r.cfg.CPTokenPublicKey are
non-empty, pass that new flag into buildEnv instead of useMCPSidecar, and only
call buildMCPSidecar/append the container when this combined flag is true so
AMBIENT_MCP_URL is not advertised when the sidecar is skipped (references:
useMCPSidecar, buildEnv, buildMCPSidecar, AMBIENT_MCP_URL).

}

pod := &unstructured.Unstructured{
Expand Down Expand Up @@ -637,6 +644,16 @@ func (r *SimpleKubeReconciler) buildEnv(ctx context.Context, session types.Sessi
}
}

if r.cfg.HTTPProxy != "" {
env = append(env, envVar("HTTP_PROXY", r.cfg.HTTPProxy))
}
if r.cfg.HTTPSProxy != "" {
env = append(env, envVar("HTTPS_PROXY", r.cfg.HTTPSProxy))
}
if r.cfg.NoProxy != "" {
env = append(env, envVar("NO_PROXY", r.cfg.NoProxy))
}
Comment on lines +647 to +655
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.

⚠️ Potential issue | 🟠 Major

Forward lowercase proxy vars too, not just uppercase.

This path only injects HTTP_PROXY / HTTPS_PROXY / NO_PROXY, and components/ambient-control-plane/internal/config/config.go Lines 81-83 only read those uppercase forms. That drops deployments configured with lowercase proxy envs, and tools based on curl/libcurl in the runner or MCP sidecar may not honor uppercase HTTP_PROXY, so outbound calls can still bypass the proxy or fail.

Suggested fix
--- a/components/ambient-control-plane/internal/config/config.go
+++ b/components/ambient-control-plane/internal/config/config.go
@@
-		HTTPProxy:            os.Getenv("HTTP_PROXY"),
-		HTTPSProxy:           os.Getenv("HTTPS_PROXY"),
-		NoProxy:              os.Getenv("NO_PROXY"),
+		HTTPProxy:            firstEnv("HTTP_PROXY", "http_proxy"),
+		HTTPSProxy:           firstEnv("HTTPS_PROXY", "https_proxy"),
+		NoProxy:              firstEnv("NO_PROXY", "no_proxy"),
 	}
@@
 func envOrDefault(key, fallback string) string {
 	if v := os.Getenv(key); v != "" {
 		return v
 	}
 	return fallback
 }
+
+func firstEnv(keys ...string) string {
+	for _, key := range keys {
+		if v := os.Getenv(key); v != "" {
+			return v
+		}
+	}
+	return ""
+}
--- a/components/ambient-control-plane/internal/reconciler/kube_reconciler.go
+++ b/components/ambient-control-plane/internal/reconciler/kube_reconciler.go
@@
 	if r.cfg.HTTPProxy != "" {
-		env = append(env, envVar("HTTP_PROXY", r.cfg.HTTPProxy))
+		env = append(env,
+			envVar("HTTP_PROXY", r.cfg.HTTPProxy),
+			envVar("http_proxy", r.cfg.HTTPProxy),
+		)
 	}
 	if r.cfg.HTTPSProxy != "" {
-		env = append(env, envVar("HTTPS_PROXY", r.cfg.HTTPSProxy))
+		env = append(env,
+			envVar("HTTPS_PROXY", r.cfg.HTTPSProxy),
+			envVar("https_proxy", r.cfg.HTTPSProxy),
+		)
 	}
 	if r.cfg.NoProxy != "" {
-		env = append(env, envVar("NO_PROXY", r.cfg.NoProxy))
+		env = append(env,
+			envVar("NO_PROXY", r.cfg.NoProxy),
+			envVar("no_proxy", r.cfg.NoProxy),
+		)
 	}

Verify the current code only handles uppercase proxy vars end-to-end:

#!/bin/bash
set -euo pipefail

echo "== Proxy env loading from config =="
rg -n 'os\.Getenv\("(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)"\)' \
  components/ambient-control-plane/internal/config/config.go

echo
echo "== Proxy env injection into pods =="
rg -n 'envVar\("(HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)"' \
  components/ambient-control-plane/internal/reconciler/kube_reconciler.go

Also applies to: 845-853

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

In `@components/ambient-control-plane/internal/reconciler/kube_reconciler.go`
around lines 647 - 655, The code only injects uppercase proxy envs
(HTTP_PROXY/HTTPS_PROXY/NO_PROXY) which misses lowercase variants; update the
reconciler to append both uppercase and lowercase names when setting env (add
envVar("http_proxy", r.cfg.HTTPProxy), envVar("https_proxy", r.cfg.HTTPSProxy),
envVar("no_proxy", r.cfg.NoProxy) alongside the existing uppercase ones in the
code that constructs env using envVar and r.cfg.HTTPProxy/HTTPSProxy/NoProxy)
and also adjust the config loader in config.go to read lowercase fallbacks
(check os.Getenv("http_proxy") etc. when populating the same config fields) so
config values set via lowercase envs are preserved end-to-end.


return env
}

Expand Down Expand Up @@ -825,6 +842,15 @@ func (r *SimpleKubeReconciler) buildMCPSidecar(sessionID string) interface{} {
envVar("AMBIENT_CP_TOKEN_PUBLIC_KEY", r.cfg.CPTokenPublicKey),
envVar("SESSION_ID", sessionID),
}
if r.cfg.HTTPProxy != "" {
env = append(env, envVar("HTTP_PROXY", r.cfg.HTTPProxy))
}
if r.cfg.HTTPSProxy != "" {
env = append(env, envVar("HTTPS_PROXY", r.cfg.HTTPSProxy))
}
if r.cfg.NoProxy != "" {
env = append(env, envVar("NO_PROXY", r.cfg.NoProxy))
}
return map[string]interface{}{
"name": "ambient-mcp",
"image": mcpImage,
Expand Down
Loading
Loading