Skip to content

Commit 6b21778

Browse files
Gkrumbach07Ambient Code Botclaude
authored
fix(backend): inherit parent userContext in child sessions (#988)
## Summary - When a runner service account creates a child session, the child now inherits the **parent session's `userContext`** instead of getting the service account identity - The runner API client automatically sets `parentSessionId` to the current session name - Fixes credential resolution (GitHub, Jira, etc.) for child sessions ## Problem Child sessions created by a runner pod had `userContext.userId` set to the service account identity (e.g., `system-serviceaccount-ns-ambient-session-session-123`). When the backend tried to resolve GitHub credentials for the child, it looked up credentials for the service account — which has none — returning 404. ## Changes **`components/backend/handlers/sessions.go`** - When `parentSessionId` is provided, fetch the parent session CR and copy its `spec.userContext` to the child - Falls back to existing identity resolution if no parent or parent lookup fails **`components/runners/ambient-runner/ambient_runner/tools/backend_api.py`** - `create_session()` now automatically sets `parentSessionId` from `AGENTIC_SESSION_NAME` env var ## Test plan - [ ] Create a session that spawns child sessions — verify child sessions have the parent's userId - [ ] Verify child sessions can resolve GitHub credentials - [ ] Verify sessions created directly (no parent) still work as before - [ ] Verify parent lookup failure (e.g., deleted parent) gracefully falls back 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Ambient Code Bot <bot@ambient-code.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7af078a commit 6b21778

4 files changed

Lines changed: 69 additions & 32 deletions

File tree

components/backend/handlers/sessions.go

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -826,45 +826,71 @@ func CreateSession(c *gin.Context) {
826826
}
827827

828828
// Add userContext from authenticated caller identity.
829-
// Prefer forwarded headers (OAuth proxy); fall back to SelfSubjectReview
829+
// When a parent session is specified, inherit the parent's userContext so that
830+
// child sessions created by a runner service account retain the original user's
831+
// identity (and therefore their credentials, e.g. GitHub tokens).
832+
// Otherwise, prefer forwarded headers (OAuth proxy); fall back to SelfSubjectReview
830833
// for headless/API callers that authenticate directly with a bearer token.
831834
{
832-
uidVal, _ := c.Get("userID")
833-
uid, _ := uidVal.(string)
834-
uid = strings.TrimSpace(uid)
835-
836-
if uid == "" {
837-
if resolved, err := resolveTokenIdentity(c.Request.Context(), reqK8s); err == nil {
838-
uid = strings.ReplaceAll(resolved, ":", "-")
839-
log.Printf("Resolved token identity via SelfSubjectReview: %s", uid)
835+
var parentUserContext map[string]interface{}
836+
837+
// If this is a child session, fetch the parent's userContext
838+
if req.ParentSessionID != "" {
839+
gvr := GetAgenticSessionV1Alpha1Resource()
840+
parentObj, err := k8sDyn.Resource(gvr).Namespace(project).Get(context.TODO(), req.ParentSessionID, v1.GetOptions{})
841+
if err != nil {
842+
log.Printf("Warning: could not fetch parent session %s/%s to inherit userContext: %v", project, req.ParentSessionID, err)
840843
} else {
841-
log.Printf("Could not resolve token identity: %v", err)
844+
uc, found, _ := unstructured.NestedMap(parentObj.Object, "spec", "userContext")
845+
if found && len(uc) > 0 {
846+
parentUserContext = uc
847+
log.Printf("Inheriting userContext from parent session %s (userId=%v)", req.ParentSessionID, uc["userId"])
848+
}
842849
}
843850
}
844851

845-
if uid != "" {
846-
displayName := ""
847-
if v, ok := c.Get("userName"); ok {
848-
if s, ok2 := v.(string); ok2 {
849-
displayName = s
852+
if parentUserContext != nil {
853+
// Use the parent's userContext directly
854+
session["spec"].(map[string]interface{})["userContext"] = parentUserContext
855+
} else {
856+
// No parent — resolve from the caller's identity
857+
uidVal, _ := c.Get("userID")
858+
uid, _ := uidVal.(string)
859+
uid = strings.TrimSpace(uid)
860+
861+
if uid == "" {
862+
if resolved, err := resolveTokenIdentity(c.Request.Context(), reqK8s); err == nil {
863+
uid = strings.ReplaceAll(resolved, ":", "-")
864+
log.Printf("Resolved token identity via SelfSubjectReview: %s", uid)
865+
} else {
866+
log.Printf("Could not resolve token identity: %v", err)
850867
}
851868
}
852-
groups := []string{}
853-
if v, ok := c.Get("userGroups"); ok {
854-
if gg, ok2 := v.([]string); ok2 {
855-
groups = gg
869+
870+
if uid != "" {
871+
displayName := ""
872+
if v, ok := c.Get("userName"); ok {
873+
if s, ok2 := v.(string); ok2 {
874+
displayName = s
875+
}
876+
}
877+
groups := []string{}
878+
if v, ok := c.Get("userGroups"); ok {
879+
if gg, ok2 := v.([]string); ok2 {
880+
groups = gg
881+
}
882+
}
883+
if displayName == "" && req.UserContext != nil {
884+
displayName = req.UserContext.DisplayName
885+
}
886+
if len(groups) == 0 && req.UserContext != nil {
887+
groups = req.UserContext.Groups
888+
}
889+
session["spec"].(map[string]interface{})["userContext"] = map[string]interface{}{
890+
"userId": uid,
891+
"displayName": displayName,
892+
"groups": groups,
856893
}
857-
}
858-
if displayName == "" && req.UserContext != nil {
859-
displayName = req.UserContext.DisplayName
860-
}
861-
if len(groups) == 0 && req.UserContext != nil {
862-
groups = req.UserContext.Groups
863-
}
864-
session["spec"].(map[string]interface{})["userContext"] = map[string]interface{}{
865-
"userId": uid,
866-
"displayName": displayName,
867-
"groups": groups,
868894
}
869895
}
870896
}

components/manifests/base/core/operator-deployment.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,10 @@ spec:
141141
resources:
142142
requests:
143143
cpu: 50m
144-
memory: 64Mi
144+
memory: 128Mi
145145
limits:
146146
cpu: 200m
147-
memory: 256Mi
147+
memory: 512Mi
148148
livenessProbe:
149149
httpGet:
150150
path: /healthz

components/manifests/overlays/production/route.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ metadata:
44
name: frontend-route
55
labels:
66
app: frontend
7+
annotations:
8+
haproxy.router.openshift.io/balance: roundrobin
9+
haproxy.router.openshift.io/disable_cookies: "true"
710
spec:
811
to:
912
kind: Service

components/runners/ambient-runner/ambient_runner/tools/backend_api.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ def create_session(
132132
) -> Dict[str, Any]:
133133
"""Create a new agentic session.
134134
135+
Automatically sets the current session as the parent so the child
136+
inherits the parent's userContext (and therefore credentials).
137+
135138
Args:
136139
session_name: Unique name for the session (must be DNS-compatible)
137140
initial_prompt: Optional initial prompt to send to the agent
@@ -148,6 +151,11 @@ def create_session(
148151
"sessionName": session_name,
149152
}
150153

154+
# Set parent session ID so the child inherits the parent's userContext
155+
parent_session = os.getenv("AGENTIC_SESSION_NAME", "").strip()
156+
if parent_session:
157+
payload["parentSessionId"] = parent_session
158+
151159
if initial_prompt:
152160
payload["initialPrompt"] = initial_prompt
153161
if display_name:

0 commit comments

Comments
 (0)