@@ -16,6 +16,7 @@ import (
1616
1717 batchv1 "k8s.io/api/batch/v1"
1818 corev1 "k8s.io/api/core/v1"
19+ rbacv1 "k8s.io/api/rbac/v1"
1920 "k8s.io/apimachinery/pkg/api/errors"
2021 v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2122 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -340,6 +341,17 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
340341 prompt , _ , _ := unstructured .NestedString (spec , "prompt" )
341342 timeout , _ , _ := unstructured .NestedInt64 (spec , "timeout" )
342343 interactive , _ , _ := unstructured .NestedBool (spec , "interactive" )
344+ workspaceImage , _ , _ := unstructured .NestedString (spec , "workspaceImage" )
345+
346+ // Workspace container mode: when workspaceImage is specified, use separate workspace container
347+ workspaceMode := workspaceImage != ""
348+ if workspaceMode {
349+ log .Printf ("Workspace container mode enabled for session %s with workspaceImage: %s" , name , workspaceImage )
350+ // Ensure runner service account with pods/exec permissions exists
351+ if err := ensureRunnerServiceAccount (sessionNamespace ); err != nil {
352+ log .Printf ("Warning: Failed to ensure runner service account: %v" , err )
353+ }
354+ }
343355
344356 llmSettings , _ , _ := unstructured .NestedMap (spec , "llmSettings" )
345357 model , _ , _ := unstructured .NestedString (llmSettings , "model" )
@@ -432,8 +444,17 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
432444 },
433445 Spec : corev1.PodSpec {
434446 RestartPolicy : corev1 .RestartPolicyNever ,
435- // Explicitly set service account for pod creation permissions
436- AutomountServiceAccountToken : boolPtr (false ),
447+ // Workspace mode needs service account token for kubectl exec
448+ AutomountServiceAccountToken : boolPtr (workspaceMode ),
449+ // Workspace mode: Enable process namespace sharing for cross-container access
450+ ShareProcessNamespace : boolPtr (workspaceMode ),
451+ // Workspace mode: Use service account with pods/exec permissions
452+ ServiceAccountName : func () string {
453+ if workspaceMode {
454+ return "ambient-runner"
455+ }
456+ return ""
457+ }(),
437458 Volumes : []corev1.Volume {
438459 {
439460 Name : "workspace" ,
@@ -538,6 +559,26 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
538559 base = append (base , corev1.EnvVar {Name : "USER_NAME" , Value : userName })
539560 }
540561
562+ // Workspace container mode: environment variables for MCP workspace exec
563+ if workspaceMode {
564+ base = append (base ,
565+ corev1.EnvVar {Name : "DISABLE_BASH_TOOL" , Value : "1" },
566+ corev1.EnvVar {Name : "WORKSPACE_CONTAINER" , Value : "workspace" },
567+ corev1.EnvVar {
568+ Name : "POD_NAME" ,
569+ ValueFrom : & corev1.EnvVarSource {
570+ FieldRef : & corev1.ObjectFieldSelector {FieldPath : "metadata.name" },
571+ },
572+ },
573+ corev1.EnvVar {
574+ Name : "POD_NAMESPACE" ,
575+ ValueFrom : & corev1.EnvVarSource {
576+ FieldRef : & corev1.ObjectFieldSelector {FieldPath : "metadata.namespace" },
577+ },
578+ },
579+ )
580+ }
581+
541582 // Platform-wide Langfuse observability configuration
542583 // Uses secretKeyRef to prevent credential exposure in pod specs
543584 // Secret is copied to session namespace from operator namespace
@@ -733,6 +774,29 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
733774 },
734775 }
735776
777+ // Workspace mode: Add workspace container with user's image
778+ if workspaceMode {
779+ workspaceContainer := corev1.Container {
780+ Name : "workspace" ,
781+ Image : workspaceImage ,
782+ ImagePullPolicy : corev1 .PullIfNotPresent ,
783+ Command : []string {"sleep" , "infinity" },
784+ WorkingDir : fmt .Sprintf ("/workspace/sessions/%s/workspace" , name ),
785+ SecurityContext : & corev1.SecurityContext {
786+ AllowPrivilegeEscalation : boolPtr (false ),
787+ Capabilities : & corev1.Capabilities {
788+ Drop : []corev1.Capability {"ALL" },
789+ },
790+ },
791+ VolumeMounts : []corev1.VolumeMount {
792+ {Name : "workspace" , MountPath : "/workspace" , ReadOnly : false },
793+ },
794+ Resources : corev1.ResourceRequirements {},
795+ }
796+ job .Spec .Template .Spec .Containers = append (job .Spec .Template .Spec .Containers , workspaceContainer )
797+ log .Printf ("Added workspace container with image: %s" , workspaceImage )
798+ }
799+
736800 // Note: No volume mounts needed for runner/integration secrets
737801 // All keys are injected as environment variables via EnvFrom above
738802
@@ -1513,6 +1577,75 @@ func deleteAmbientLangfuseSecret(ctx context.Context, namespace string) error {
15131577 return nil
15141578}
15151579
1580+ // ensureRunnerServiceAccount creates or updates the ambient-runner service account and RBAC
1581+ // resources needed for workspace container mode (kubectl exec permissions).
1582+ func ensureRunnerServiceAccount (namespace string ) error {
1583+ ctx := context .Background ()
1584+
1585+ // Create/Update ServiceAccount
1586+ sa := & corev1.ServiceAccount {
1587+ ObjectMeta : v1.ObjectMeta {
1588+ Name : "ambient-runner" ,
1589+ Namespace : namespace ,
1590+ },
1591+ }
1592+ _ , err := config .K8sClient .CoreV1 ().ServiceAccounts (namespace ).Create (ctx , sa , v1.CreateOptions {})
1593+ if err != nil && ! errors .IsAlreadyExists (err ) {
1594+ return fmt .Errorf ("failed to create service account: %w" , err )
1595+ }
1596+
1597+ // Create/Update Role with pods/exec permission
1598+ role := & rbacv1.Role {
1599+ ObjectMeta : v1.ObjectMeta {
1600+ Name : "ambient-runner-exec" ,
1601+ Namespace : namespace ,
1602+ },
1603+ Rules : []rbacv1.PolicyRule {
1604+ {
1605+ APIGroups : []string {"" },
1606+ Resources : []string {"pods/exec" },
1607+ Verbs : []string {"create" },
1608+ },
1609+ {
1610+ APIGroups : []string {"" },
1611+ Resources : []string {"pods" },
1612+ Verbs : []string {"get" , "list" },
1613+ },
1614+ },
1615+ }
1616+ _ , err = config .K8sClient .RbacV1 ().Roles (namespace ).Create (ctx , role , v1.CreateOptions {})
1617+ if err != nil && ! errors .IsAlreadyExists (err ) {
1618+ return fmt .Errorf ("failed to create role: %w" , err )
1619+ }
1620+
1621+ // Create/Update RoleBinding
1622+ rb := & rbacv1.RoleBinding {
1623+ ObjectMeta : v1.ObjectMeta {
1624+ Name : "ambient-runner-exec" ,
1625+ Namespace : namespace ,
1626+ },
1627+ RoleRef : rbacv1.RoleRef {
1628+ APIGroup : "rbac.authorization.k8s.io" ,
1629+ Kind : "Role" ,
1630+ Name : "ambient-runner-exec" ,
1631+ },
1632+ Subjects : []rbacv1.Subject {
1633+ {
1634+ Kind : "ServiceAccount" ,
1635+ Name : "ambient-runner" ,
1636+ Namespace : namespace ,
1637+ },
1638+ },
1639+ }
1640+ _ , err = config .K8sClient .RbacV1 ().RoleBindings (namespace ).Create (ctx , rb , v1.CreateOptions {})
1641+ if err != nil && ! errors .IsAlreadyExists (err ) {
1642+ return fmt .Errorf ("failed to create role binding: %w" , err )
1643+ }
1644+
1645+ log .Printf ("Ensured ambient-runner RBAC resources in namespace %s" , namespace )
1646+ return nil
1647+ }
1648+
15161649// Helper functions
15171650var (
15181651 boolPtr = func (b bool ) * bool { return & b }
0 commit comments