diff --git a/components/operator/internal/handlers/sessions.go b/components/operator/internal/handlers/sessions.go index 862d8202..c67fae7f 100644 --- a/components/operator/internal/handlers/sessions.go +++ b/components/operator/internal/handlers/sessions.go @@ -1376,35 +1376,69 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error { } } - // Check for Google OAuth secret and mount it if present (for MCP Google Drive integration) + // Create placeholder Google OAuth secret if it doesn't exist (for MCP Google Workspace integration) + // This ensures the volume mount is always present so K8s can sync credentials after OAuth completion googleOAuthSecretName := fmt.Sprintf("%s-google-oauth", name) - if _, err := config.K8sClient.CoreV1().Secrets(sessionNamespace).Get(context.TODO(), googleOAuthSecretName, v1.GetOptions{}); err == nil { - log.Printf("Found Google OAuth secret %s, mounting to runner container", googleOAuthSecretName) - job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: "google-oauth", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: googleOAuthSecretName, - Optional: boolPtr(true), // Don't fail if secret disappears before pod starts + if _, err := config.K8sClient.CoreV1().Secrets(sessionNamespace).Get(context.TODO(), googleOAuthSecretName, v1.GetOptions{}); errors.IsNotFound(err) { + // Create empty placeholder secret - backend will populate it after user completes OAuth + placeholderSecret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: googleOAuthSecretName, + Namespace: sessionNamespace, + Labels: map[string]string{ + "app": "ambient-code", + "ambient-code.io/session": name, + "ambient-code.io/provider": "google", + "ambient-code.io/oauth": "placeholder", + }, + OwnerReferences: []v1.OwnerReference{ + { + APIVersion: "vteam.ambient-code/v1", + Kind: "AgenticSession", + Name: currentObj.GetName(), + UID: currentObj.GetUID(), + Controller: boolPtr(true), + }, }, }, - }) - // Mount to the ambient-code-runner container - for i := range job.Spec.Template.Spec.Containers { - if job.Spec.Template.Spec.Containers[i].Name == "ambient-code-runner" { - job.Spec.Template.Spec.Containers[i].VolumeMounts = append(job.Spec.Template.Spec.Containers[i].VolumeMounts, corev1.VolumeMount{ - Name: "google-oauth", - MountPath: "/app/.google_workspace_mcp/credentials", - ReadOnly: true, - }) - log.Printf("Mounted Google OAuth secret to /app/.google_workspace_mcp/credentials in runner container for session %s", name) - break - } + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "credentials.json": []byte(""), // Empty placeholder, runner checks for content + }, } - } else if !errors.IsNotFound(err) { - log.Printf("Error checking for Google OAuth secret %s: %v (continuing without MCP)", googleOAuthSecretName, err) + if _, createErr := config.K8sClient.CoreV1().Secrets(sessionNamespace).Create(context.TODO(), placeholderSecret, v1.CreateOptions{}); createErr != nil { + log.Printf("Warning: Failed to create placeholder Google OAuth secret %s: %v", googleOAuthSecretName, createErr) + } else { + log.Printf("Created placeholder Google OAuth secret %s (will be populated after user OAuth)", googleOAuthSecretName) + } + } else if err != nil { + log.Printf("Error checking for Google OAuth secret %s: %v", googleOAuthSecretName, err) } else { - log.Printf("No Google OAuth secret found (session %s), MCP Google Drive integration will not be available", name) + log.Printf("Found existing Google OAuth secret %s", googleOAuthSecretName) + } + + // Always mount Google OAuth secret (with Optional: true so pod starts even if empty) + // K8s will sync updates when backend populates credentials after OAuth completion (~60s) + job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: "google-oauth", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: googleOAuthSecretName, + Optional: boolPtr(true), // Don't fail if secret is empty or missing + }, + }, + }) + // Mount to the ambient-code-runner container + for i := range job.Spec.Template.Spec.Containers { + if job.Spec.Template.Spec.Containers[i].Name == "ambient-code-runner" { + job.Spec.Template.Spec.Containers[i].VolumeMounts = append(job.Spec.Template.Spec.Containers[i].VolumeMounts, corev1.VolumeMount{ + Name: "google-oauth", + MountPath: "/app/.google_workspace_mcp/credentials", + ReadOnly: true, + }) + log.Printf("Mounted Google OAuth secret to /app/.google_workspace_mcp/credentials in runner container for session %s", name) + break + } } // Do not mount runner Secret volume; runner fetches tokens on demand diff --git a/components/runners/claude-code-runner/main.py b/components/runners/claude-code-runner/main.py index dee4e6c9..7f14b166 100644 --- a/components/runners/claude-code-runner/main.py +++ b/components/runners/claude-code-runner/main.py @@ -80,7 +80,6 @@ async def lifespan(app: FastAPI): # Import adapter here to avoid circular imports from adapter import ClaudeCodeAdapter - from pathlib import Path # Initialize context from environment session_id = os.getenv("SESSION_ID", "unknown") @@ -98,16 +97,17 @@ async def lifespan(app: FastAPI): logger.info("Adapter initialized - fresh client will be created for each run") - # Check if this is a restart (conversation history exists) - history_marker = Path(workspace_path) / ".claude" / "state" + # Check if this is a continuation (has parent session) + # PARENT_SESSION_ID is set when continuing from another session + parent_session_id = os.getenv("PARENT_SESSION_ID", "").strip() - # Check for INITIAL_PROMPT and auto-execute (only if this is first run) + # Check for INITIAL_PROMPT and auto-execute (only if no parent session) initial_prompt = os.getenv("INITIAL_PROMPT", "").strip() - if initial_prompt and not history_marker.exists(): + if initial_prompt and not parent_session_id: logger.info(f"INITIAL_PROMPT detected ({len(initial_prompt)} chars), will auto-execute after 3s delay") asyncio.create_task(auto_execute_initial_prompt(initial_prompt, session_id)) elif initial_prompt: - logger.info(f"INITIAL_PROMPT detected but conversation history exists - skipping auto-execution (session restart)") + logger.info(f"INITIAL_PROMPT detected but has parent session ({parent_session_id[:12]}...) - skipping") logger.info(f"AG-UI server ready for session {session_id}") @@ -122,6 +122,8 @@ async def auto_execute_initial_prompt(prompt: str, session_id: str): The 3-second delay gives the runner time to fully start. Backend has retry logic to handle if Service DNS isn't ready yet. + + Only called for fresh sessions (no PARENT_SESSION_ID set). """ import uuid import aiohttp