Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 58 additions & 24 deletions components/operator/internal/handlers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions components/runners/claude-code-runner/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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}")

Expand All @@ -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
Expand Down
Loading