@@ -409,6 +409,8 @@ func ListSessions(c *gin.Context) {
409409 session .Status = parseStatus (status )
410410 }
411411
412+ session .AutoBranch = ComputeAutoBranch (item .GetName ())
413+
412414 sessions = append (sessions , session )
413415 }
414416
@@ -553,9 +555,10 @@ func CreateSession(c *gin.Context) {
553555 timeout = * req .Timeout
554556 }
555557
556- // Generate unique name
558+ // Generate unique name (timestamp-based)
559+ // Note: Runner will create branch as "ambient/{session-name}"
557560 timestamp := time .Now ().Unix ()
558- name := fmt .Sprintf ("agentic- session-%d" , timestamp )
561+ name := fmt .Sprintf ("session-%d" , timestamp )
559562
560563 // Create the custom resource
561564 // Metadata
@@ -638,8 +641,11 @@ func CreateSession(c *gin.Context) {
638641 arr := make ([]map [string ]interface {}, 0 , len (req .Repos ))
639642 for _ , r := range req .Repos {
640643 m := map [string ]interface {}{"url" : r .URL }
641- if r .Branch != nil {
644+ // Fill in branch if not provided (auto-generate from session name)
645+ if r .Branch != nil && strings .TrimSpace (* r .Branch ) != "" {
642646 m ["branch" ] = * r .Branch
647+ } else {
648+ m ["branch" ] = ComputeAutoBranch (name )
643649 }
644650 if r .AutoPush != nil {
645651 m ["autoPush" ] = * r .AutoPush
@@ -722,9 +728,10 @@ func CreateSession(c *gin.Context) {
722728 // This ensures consistent behavior whether sessions are created via API or kubectl.
723729
724730 c .JSON (http .StatusCreated , gin.H {
725- "message" : "Agentic session created successfully" ,
726- "name" : name ,
727- "uid" : created .GetUID (),
731+ "message" : "Agentic session created successfully" ,
732+ "name" : name ,
733+ "uid" : created .GetUID (),
734+ "autoBranch" : ComputeAutoBranch (name ),
728735 })
729736}
730737
@@ -773,6 +780,8 @@ func GetSession(c *gin.Context) {
773780 session .Status = parseStatus (status )
774781 }
775782
783+ session .AutoBranch = ComputeAutoBranch (sessionName )
784+
776785 c .JSON (http .StatusOK , session )
777786}
778787
@@ -1459,19 +1468,77 @@ func RemoveRepo(c *gin.Context) {
14591468 repos , _ := spec ["repos" ].([]interface {})
14601469
14611470 filteredRepos := []interface {}{}
1462- found := false
1471+ foundInSpec := false
14631472 for _ , r := range repos {
14641473 rm , _ := r .(map [string ]interface {})
14651474 url , _ := rm ["url" ].(string )
14661475 if DeriveRepoFolderFromURL (url ) != repoName {
14671476 filteredRepos = append (filteredRepos , r )
14681477 } else {
1469- found = true
1478+ foundInSpec = true
14701479 }
14711480 }
14721481
1473- if ! found {
1474- c .JSON (http .StatusNotFound , gin.H {"error" : "Repository not found in session" })
1482+ // Also check status.reconciledRepos for repos added directly to runner
1483+ status , found , err := unstructured .NestedMap (item .Object , "status" )
1484+ if ! found || err != nil {
1485+ log .Printf ("Failed to get status: %v" , err )
1486+ status = make (map [string ]interface {})
1487+ }
1488+
1489+ reconciledRepos , found , err := unstructured .NestedSlice (status , "reconciledRepos" )
1490+ if ! found || err != nil {
1491+ log .Printf ("Failed to get reconciledRepos: %v" , err )
1492+ reconciledRepos = []interface {}{}
1493+ }
1494+
1495+ foundInReconciled := false
1496+ for _ , r := range reconciledRepos {
1497+ rm , ok := r .(map [string ]interface {})
1498+ if ! ok {
1499+ continue
1500+ }
1501+
1502+ name , found , err := unstructured .NestedString (rm , "name" )
1503+ if found && err == nil && name == repoName {
1504+ foundInReconciled = true
1505+ break
1506+ }
1507+
1508+ // Also try matching by URL
1509+ url , found , err := unstructured .NestedString (rm , "url" )
1510+ if found && err == nil && DeriveRepoFolderFromURL (url ) == repoName {
1511+ foundInReconciled = true
1512+ break
1513+ }
1514+ }
1515+
1516+ // Always call runner to remove from filesystem (if session is running)
1517+ // Do this BEFORE checking if repo exists in CR, because it might only be on filesystem
1518+ phase , _ , _ := unstructured .NestedString (status , "phase" )
1519+ runnerRemoved := false
1520+ if phase == "Running" {
1521+ runnerURL := fmt .Sprintf ("http://session-%s.%s.svc.cluster.local:8001/repos/remove" , sessionName , project )
1522+ runnerReq := map [string ]string {"name" : repoName }
1523+ reqBody , _ := json .Marshal (runnerReq )
1524+ resp , err := http .Post (runnerURL , "application/json" , bytes .NewReader (reqBody ))
1525+ if err != nil {
1526+ log .Printf ("Warning: failed to call runner /repos/remove: %v" , err )
1527+ } else {
1528+ defer resp .Body .Close ()
1529+ if resp .StatusCode == http .StatusOK {
1530+ runnerRemoved = true
1531+ log .Printf ("Runner successfully removed repo %s from filesystem" , repoName )
1532+ } else {
1533+ body , _ := io .ReadAll (resp .Body )
1534+ log .Printf ("Runner failed to remove repo %s (status %d): %s" , repoName , resp .StatusCode , string (body ))
1535+ }
1536+ }
1537+ }
1538+
1539+ // Allow delete if repo is in CR OR was successfully removed from runner
1540+ if ! foundInSpec && ! foundInReconciled && ! runnerRemoved {
1541+ c .JSON (http .StatusNotFound , gin.H {"error" : "Repository not found in session or runner" })
14751542 return
14761543 }
14771544
@@ -3069,6 +3136,77 @@ func DiffSessionRepo(c *gin.Context) {
30693136 c .Data (resp .StatusCode , resp .Header .Get ("Content-Type" ), bodyBytes )
30703137}
30713138
3139+ // GetReposStatus returns current status of all repositories (branches, current branch, etc.)
3140+ // GET /api/projects/:projectName/agentic-sessions/:sessionName/repos/status
3141+ func GetReposStatus (c * gin.Context ) {
3142+ project := c .Param ("projectName" )
3143+ session := c .Param ("sessionName" )
3144+
3145+ k8sClt , dynClt := GetK8sClientsForRequest (c )
3146+ if k8sClt == nil {
3147+ c .JSON (http .StatusUnauthorized , gin.H {"error" : "Invalid or missing token" })
3148+ c .Abort ()
3149+ return
3150+ }
3151+
3152+ // Verify user has access to the session using user-scoped K8s client
3153+ // This ensures RBAC is enforced before we call the runner
3154+ gvr := GetAgenticSessionV1Alpha1Resource ()
3155+ _ , err := dynClt .Resource (gvr ).Namespace (project ).Get (context .TODO (), session , v1.GetOptions {})
3156+ if errors .IsNotFound (err ) {
3157+ c .JSON (http .StatusNotFound , gin.H {"error" : "Session not found" })
3158+ return
3159+ }
3160+ if err != nil {
3161+ log .Printf ("GetReposStatus: failed to verify session access: %v" , err )
3162+ c .JSON (http .StatusForbidden , gin.H {"error" : "Access denied" })
3163+ return
3164+ }
3165+
3166+ // Call runner's /repos/status endpoint directly
3167+ // Authentication flow:
3168+ // 1. Backend validated user has access to session (above)
3169+ // 2. Backend calls runner as trusted internal service (no auth header forwarding)
3170+ // 3. Runner trusts backend's validation
3171+ // Port 8001 matches AG-UI Service defined in operator (sessions.go:1384)
3172+ // If changing this port, also update: operator containerPort, Service port, and AGUI_PORT env
3173+ runnerURL := fmt .Sprintf ("http://session-%s.%s.svc.cluster.local:8001/repos/status" , session , project )
3174+
3175+ req , err := http .NewRequestWithContext (c .Request .Context (), http .MethodGet , runnerURL , nil )
3176+ if err != nil {
3177+ log .Printf ("GetReposStatus: failed to create HTTP request: %v" , err )
3178+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Failed to create request" })
3179+ return
3180+ }
3181+ // NOTE: Do NOT forward Authorization header to runner (matches pattern of AddWorkflow, AddRepository, RemoveRepo)
3182+ // Runner is treated as a trusted backend service; RBAC enforcement happens in backend
3183+
3184+ client := & http.Client {Timeout : 5 * time .Second }
3185+ resp , err := client .Do (req )
3186+ if err != nil {
3187+ log .Printf ("GetReposStatus: runner not reachable: %v" , err )
3188+ // Return empty repos list instead of error for better UX
3189+ c .JSON (http .StatusOK , gin.H {"repos" : []interface {}{}})
3190+ return
3191+ }
3192+ defer resp .Body .Close ()
3193+
3194+ bodyBytes , err := io .ReadAll (resp .Body )
3195+ if err != nil {
3196+ log .Printf ("GetReposStatus: failed to read response body: %v" , err )
3197+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Failed to read response from runner" })
3198+ return
3199+ }
3200+
3201+ if resp .StatusCode != http .StatusOK {
3202+ log .Printf ("GetReposStatus: runner returned status %d" , resp .StatusCode )
3203+ c .JSON (http .StatusOK , gin.H {"repos" : []interface {}{}})
3204+ return
3205+ }
3206+
3207+ c .Data (http .StatusOK , "application/json" , bodyBytes )
3208+ }
3209+
30723210// GetGitStatus returns git status for a directory in the workspace
30733211// GET /api/projects/:projectName/agentic-sessions/:sessionName/git/status?path=artifacts
30743212func GetGitStatus (c * gin.Context ) {
0 commit comments