@@ -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,78 @@ 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+ // Note: status map is read-only here, not persisted back to CR
1484+ status , found , err := unstructured .NestedMap (item .Object , "status" )
1485+ if ! found || err != nil {
1486+ log .Printf ("Failed to get status: %v" , err )
1487+ status = make (map [string ]interface {}) // Local empty map for safe reads
1488+ }
1489+
1490+ reconciledRepos , found , err := unstructured .NestedSlice (status , "reconciledRepos" )
1491+ if ! found || err != nil {
1492+ log .Printf ("Failed to get reconciledRepos: %v" , err )
1493+ reconciledRepos = []interface {}{}
1494+ }
1495+
1496+ foundInReconciled := false
1497+ for _ , r := range reconciledRepos {
1498+ rm , ok := r .(map [string ]interface {})
1499+ if ! ok {
1500+ continue
1501+ }
1502+
1503+ name , found , err := unstructured .NestedString (rm , "name" )
1504+ if found && err == nil && name == repoName {
1505+ foundInReconciled = true
1506+ break
1507+ }
1508+
1509+ // Also try matching by URL
1510+ url , found , err := unstructured .NestedString (rm , "url" )
1511+ if found && err == nil && DeriveRepoFolderFromURL (url ) == repoName {
1512+ foundInReconciled = true
1513+ break
1514+ }
1515+ }
1516+
1517+ // Always call runner to remove from filesystem (if session is running)
1518+ // Do this BEFORE checking if repo exists in CR, because it might only be on filesystem
1519+ phase , _ , _ := unstructured .NestedString (status , "phase" )
1520+ runnerRemoved := false
1521+ if phase == "Running" {
1522+ runnerURL := fmt .Sprintf ("http://session-%s.%s.svc.cluster.local:8001/repos/remove" , sessionName , project )
1523+ runnerReq := map [string ]string {"name" : repoName }
1524+ reqBody , _ := json .Marshal (runnerReq )
1525+ resp , err := http .Post (runnerURL , "application/json" , bytes .NewReader (reqBody ))
1526+ if err != nil {
1527+ log .Printf ("Warning: failed to call runner /repos/remove: %v" , err )
1528+ } else {
1529+ defer resp .Body .Close ()
1530+ if resp .StatusCode == http .StatusOK {
1531+ runnerRemoved = true
1532+ log .Printf ("Runner successfully removed repo %s from filesystem" , repoName )
1533+ } else {
1534+ body , _ := io .ReadAll (resp .Body )
1535+ log .Printf ("Runner failed to remove repo %s (status %d): %s" , repoName , resp .StatusCode , string (body ))
1536+ }
1537+ }
1538+ }
1539+
1540+ // Allow delete if repo is in CR OR was successfully removed from runner
1541+ if ! foundInSpec && ! foundInReconciled && ! runnerRemoved {
1542+ c .JSON (http .StatusNotFound , gin.H {"error" : "Repository not found in session or runner" })
14751543 return
14761544 }
14771545
@@ -3069,6 +3137,77 @@ func DiffSessionRepo(c *gin.Context) {
30693137 c .Data (resp .StatusCode , resp .Header .Get ("Content-Type" ), bodyBytes )
30703138}
30713139
3140+ // GetReposStatus returns current status of all repositories (branches, current branch, etc.)
3141+ // GET /api/projects/:projectName/agentic-sessions/:sessionName/repos/status
3142+ func GetReposStatus (c * gin.Context ) {
3143+ project := c .Param ("projectName" )
3144+ session := c .Param ("sessionName" )
3145+
3146+ k8sClt , dynClt := GetK8sClientsForRequest (c )
3147+ if k8sClt == nil {
3148+ c .JSON (http .StatusUnauthorized , gin.H {"error" : "Invalid or missing token" })
3149+ c .Abort ()
3150+ return
3151+ }
3152+
3153+ // Verify user has access to the session using user-scoped K8s client
3154+ // This ensures RBAC is enforced before we call the runner
3155+ gvr := GetAgenticSessionV1Alpha1Resource ()
3156+ _ , err := dynClt .Resource (gvr ).Namespace (project ).Get (context .TODO (), session , v1.GetOptions {})
3157+ if errors .IsNotFound (err ) {
3158+ c .JSON (http .StatusNotFound , gin.H {"error" : "Session not found" })
3159+ return
3160+ }
3161+ if err != nil {
3162+ log .Printf ("GetReposStatus: failed to verify session access: %v" , err )
3163+ c .JSON (http .StatusForbidden , gin.H {"error" : "Access denied" })
3164+ return
3165+ }
3166+
3167+ // Call runner's /repos/status endpoint directly
3168+ // Authentication flow:
3169+ // 1. Backend validated user has access to session (above)
3170+ // 2. Backend calls runner as trusted internal service (no auth header forwarding)
3171+ // 3. Runner trusts backend's validation
3172+ // Port 8001 matches AG-UI Service defined in operator (sessions.go:1384)
3173+ // If changing this port, also update: operator containerPort, Service port, and AGUI_PORT env
3174+ runnerURL := fmt .Sprintf ("http://session-%s.%s.svc.cluster.local:8001/repos/status" , session , project )
3175+
3176+ req , err := http .NewRequestWithContext (c .Request .Context (), http .MethodGet , runnerURL , nil )
3177+ if err != nil {
3178+ log .Printf ("GetReposStatus: failed to create HTTP request: %v" , err )
3179+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Failed to create request" })
3180+ return
3181+ }
3182+ // NOTE: Do NOT forward Authorization header to runner (matches pattern of AddWorkflow, AddRepository, RemoveRepo)
3183+ // Runner is treated as a trusted backend service; RBAC enforcement happens in backend
3184+
3185+ client := & http.Client {Timeout : 5 * time .Second }
3186+ resp , err := client .Do (req )
3187+ if err != nil {
3188+ log .Printf ("GetReposStatus: runner not reachable: %v" , err )
3189+ // Return empty repos list instead of error for better UX
3190+ c .JSON (http .StatusOK , gin.H {"repos" : []interface {}{}})
3191+ return
3192+ }
3193+ defer resp .Body .Close ()
3194+
3195+ bodyBytes , err := io .ReadAll (resp .Body )
3196+ if err != nil {
3197+ log .Printf ("GetReposStatus: failed to read response body: %v" , err )
3198+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Failed to read response from runner" })
3199+ return
3200+ }
3201+
3202+ if resp .StatusCode != http .StatusOK {
3203+ log .Printf ("GetReposStatus: runner returned status %d" , resp .StatusCode )
3204+ c .JSON (http .StatusOK , gin.H {"repos" : []interface {}{}})
3205+ return
3206+ }
3207+
3208+ c .Data (http .StatusOK , "application/json" , bodyBytes )
3209+ }
3210+
30723211// GetGitStatus returns git status for a directory in the workspace
30733212// GET /api/projects/:projectName/agentic-sessions/:sessionName/git/status?path=artifacts
30743213func GetGitStatus (c * gin.Context ) {
0 commit comments