@@ -599,41 +599,135 @@ func CreateSession(c *gin.Context) {
599599 if metadata ["annotations" ] == nil {
600600 metadata ["annotations" ] = make (map [string ]interface {})
601601 }
602- annotations := metadata ["annotations" ].(map [string ]interface {})
602+ annotations , found , err := unstructured .NestedMap (metadata , "annotations" )
603+ if err != nil || ! found {
604+ log .Printf ("Failed to get annotations from metadata for parent session %s: %v" , req .ParentSessionID , err )
605+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session metadata" })
606+ return
607+ }
603608 annotations ["vteam.ambient-code/parent-session-id" ] = req .ParentSessionID
609+ // Persist annotations back to metadata
610+ if err := unstructured .SetNestedMap (metadata , annotations , "annotations" ); err != nil {
611+ log .Printf ("Failed to set annotations for parent session %s: %v" , req .ParentSessionID , err )
612+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session metadata" })
613+ return
614+ }
615+ // Persist metadata back to session
616+ if err := unstructured .SetNestedMap (session , metadata , "metadata" ); err != nil {
617+ log .Printf ("Failed to set metadata for session %s: %v" , name , err )
618+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session metadata" })
619+ return
620+ }
604621 log .Printf ("Creating continuation session from parent %s (operator will handle temp pod cleanup)" , req .ParentSessionID )
605622 // Note: Operator will delete temp pod when session starts (desired-phase=Running)
606623 }
607624
608625 if len (envVars ) > 0 {
609- spec := session ["spec" ].(map [string ]interface {})
626+ spec , found , err := unstructured .NestedMap (session , "spec" )
627+ if err != nil || ! found {
628+ log .Printf ("Failed to get spec from session %s: %v" , name , err )
629+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
630+ return
631+ }
610632 spec ["environmentVariables" ] = envVars
633+ // Persist spec back to session
634+ if err := unstructured .SetNestedMap (session , spec , "spec" ); err != nil {
635+ log .Printf ("Failed to set spec for session %s: %v" , name , err )
636+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
637+ return
638+ }
611639 }
612640
613641 // Interactive flag
614642 if req .Interactive != nil {
615- session ["spec" ].(map [string ]interface {})["interactive" ] = * req .Interactive
643+ spec , found , err := unstructured .NestedMap (session , "spec" )
644+ if err != nil || ! found {
645+ log .Printf ("Failed to get spec from session %s: %v" , name , err )
646+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
647+ return
648+ }
649+ spec ["interactive" ] = * req .Interactive
650+ // Persist spec back to session
651+ if err := unstructured .SetNestedMap (session , spec , "spec" ); err != nil {
652+ log .Printf ("Failed to set spec for session %s: %v" , name , err )
653+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
654+ return
655+ }
616656 }
617657
618658 // AutoPushOnComplete flag
619659 if req .AutoPushOnComplete != nil {
620- session ["spec" ].(map [string ]interface {})["autoPushOnComplete" ] = * req .AutoPushOnComplete
660+ spec , found , err := unstructured .NestedMap (session , "spec" )
661+ if err != nil || ! found {
662+ log .Printf ("Failed to get spec from session %s: %v" , name , err )
663+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
664+ return
665+ }
666+ spec ["autoPushOnComplete" ] = * req .AutoPushOnComplete
667+ // Persist spec back to session
668+ if err := unstructured .SetNestedMap (session , spec , "spec" ); err != nil {
669+ log .Printf ("Failed to set spec for session %s: %v" , name , err )
670+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
671+ return
672+ }
621673 }
622674
623675 // Set multi-repo configuration on spec (simplified format)
676+ // Generate working branch names upfront based on session context
624677 {
625- spec := session ["spec" ].(map [string ]interface {})
678+ spec , found , err := unstructured .NestedMap (session , "spec" )
679+ if err != nil || ! found {
680+ log .Printf ("Failed to get spec from session %s: %v" , name , err )
681+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
682+ return
683+ }
626684 if len (req .Repos ) > 0 {
627685 arr := make ([]map [string ]interface {}, 0 , len (req .Repos ))
628686 for _ , r := range req .Repos {
629- m := map [string ]interface {}{"url" : r .URL }
630- if r .Branch != nil {
631- m ["branch" ] = * r .Branch
687+ // Determine the working branch to use
688+ var requestedBranch string
689+ if r .WorkingBranch != nil {
690+ requestedBranch = strings .TrimSpace (* r .WorkingBranch )
691+ } else if r .Branch != nil {
692+ requestedBranch = strings .TrimSpace (* r .Branch )
693+ }
694+
695+ // Validate user-supplied branch names to prevent command injection
696+ if requestedBranch != "" && ! isValidGitBranchName (requestedBranch ) {
697+ c .JSON (http .StatusBadRequest , gin.H {"error" : fmt .Sprintf ("Invalid branch name format: %s" , requestedBranch )})
698+ return
699+ }
700+
701+ allowProtected := false
702+ if r .AllowProtectedWork != nil {
703+ allowProtected = * r .AllowProtectedWork
704+ }
705+
706+ // Generate the actual branch name that will be used
707+ workingBranch := generateWorkingBranch (
708+ req .DisplayName ,
709+ name , // session name (unique ID)
710+ requestedBranch ,
711+ allowProtected ,
712+ )
713+
714+ // Wrap in 'input' object to match runner expectations
715+ m := map [string ]interface {}{
716+ "input" : map [string ]interface {}{
717+ "url" : r .URL ,
718+ "branch" : workingBranch ,
719+ },
632720 }
633721 arr = append (arr , m )
634722 }
635723 spec ["repos" ] = arr
636724 }
725+ // Persist spec back to session
726+ if err := unstructured .SetNestedMap (session , spec , "spec" ); err != nil {
727+ log .Printf ("Failed to set spec for session %s: %v" , name , err )
728+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
729+ return
730+ }
637731 }
638732
639733 // Add userContext derived from authenticated caller; ignore client-supplied userId
@@ -661,11 +755,23 @@ func CreateSession(c *gin.Context) {
661755 if len (groups ) == 0 && req .UserContext != nil {
662756 groups = req .UserContext .Groups
663757 }
664- session ["spec" ].(map [string ]interface {})["userContext" ] = map [string ]interface {}{
758+ spec , found , err := unstructured .NestedMap (session , "spec" )
759+ if err != nil || ! found {
760+ log .Printf ("Failed to get spec from session %s: %v" , name , err )
761+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
762+ return
763+ }
764+ spec ["userContext" ] = map [string ]interface {}{
665765 "userId" : uid ,
666766 "displayName" : displayName ,
667767 "groups" : groups ,
668768 }
769+ // Persist spec back to session
770+ if err := unstructured .SetNestedMap (session , spec , "spec" ); err != nil {
771+ log .Printf ("Failed to set spec for session %s: %v" , name , err )
772+ c .JSON (http .StatusInternalServerError , gin.H {"error" : "Invalid session spec" })
773+ return
774+ }
669775 }
670776 }
671777
@@ -1406,19 +1512,21 @@ func AddRepo(c *gin.Context) {
14061512 }
14071513
14081514 var req struct {
1409- URL string `json:"url" binding:"required"`
1410- Branch string `json:"branch"`
1515+ URL string `json:"url" binding:"required"`
1516+ Branch string `json:"branch"`
1517+ WorkingBranch string `json:"workingBranch"`
1518+ AllowProtectedWork bool `json:"allowProtectedWork"`
1519+ Sync * struct {
1520+ URL string `json:"url"`
1521+ Branch string `json:"branch"`
1522+ } `json:"sync"`
14111523 }
14121524
14131525 if err := c .ShouldBindJSON (& req ); err != nil {
14141526 c .JSON (http .StatusBadRequest , gin.H {"error" : err .Error ()})
14151527 return
14161528 }
14171529
1418- if req .Branch == "" {
1419- req .Branch = "main"
1420- }
1421-
14221530 gvr := GetAgenticSessionV1Alpha1Resource ()
14231531 item , err := k8sDyn .Resource (gvr ).Namespace (project ).Get (context .TODO (), sessionName , v1.GetOptions {})
14241532 if err != nil {
@@ -1447,10 +1555,52 @@ func AddRepo(c *gin.Context) {
14471555 repos = []interface {}{}
14481556 }
14491557
1558+ // Determine the requested branch
1559+ requestedBranch := req .WorkingBranch
1560+ if requestedBranch == "" {
1561+ requestedBranch = req .Branch
1562+ }
1563+
1564+ // Validate user-supplied branch names to prevent command injection
1565+ if requestedBranch != "" && ! isValidGitBranchName (requestedBranch ) {
1566+ c .JSON (http .StatusBadRequest , gin.H {"error" : fmt .Sprintf ("Invalid branch name format: %s" , requestedBranch )})
1567+ return
1568+ }
1569+
1570+ // Get session display name for branch generation
1571+ displayName := ""
1572+ if dn , ok := spec ["displayName" ].(string ); ok {
1573+ displayName = dn
1574+ }
1575+
1576+ // Generate the actual working branch name
1577+ workingBranch := generateWorkingBranch (
1578+ displayName ,
1579+ sessionName ,
1580+ requestedBranch ,
1581+ req .AllowProtectedWork ,
1582+ )
1583+
1584+ // Wrap in 'input' object to match runner expectations
14501585 newRepo := map [string ]interface {}{
1451- "url" : req .URL ,
1452- "branch" : req .Branch ,
1586+ "input" : map [string ]interface {}{
1587+ "url" : req .URL ,
1588+ "branch" : workingBranch ,
1589+ },
1590+ }
1591+
1592+ // Add sync configuration if provided
1593+ if req .Sync != nil && strings .TrimSpace (req .Sync .URL ) != "" {
1594+ syncBranch := strings .TrimSpace (req .Sync .Branch )
1595+ if syncBranch == "" {
1596+ syncBranch = "main"
1597+ }
1598+ newRepo ["sync" ] = map [string ]interface {}{
1599+ "url" : strings .TrimSpace (req .Sync .URL ),
1600+ "branch" : syncBranch ,
1601+ }
14531602 }
1603+
14541604 repos = append (repos , newRepo )
14551605 spec ["repos" ] = repos
14561606
0 commit comments