Skip to content
Open
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
45 changes: 26 additions & 19 deletions components/operator/internal/handlers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,17 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
}
log.Printf("Session %s initiated by user: %s (userId: %s)", name, userName, userID)

// Determine runner token secret name for volume mount
runnerTokenSecretName := ""
if annotations := currentObj.GetAnnotations(); annotations != nil {
if v, ok := annotations["ambient-code.io/runner-token-secret"]; ok && strings.TrimSpace(v) != "" {
runnerTokenSecretName = strings.TrimSpace(v)
}
}
if runnerTokenSecretName == "" {
runnerTokenSecretName = fmt.Sprintf("ambient-runner-token-%s", name)
}

// Create the Job
job := &batchv1.Job{
ObjectMeta: v1.ObjectMeta{
Expand Down Expand Up @@ -979,6 +990,14 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
},
},
},
{
Name: "runner-token",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: runnerTokenSecretName,
},
},
},
},

// InitContainer to ensure workspace directory structure exists
Expand Down Expand Up @@ -1037,6 +1056,8 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
// Mount .claude directory for session state persistence
// This enables SDK's built-in resume functionality
{Name: "workspace", MountPath: "/app/.claude", SubPath: fmt.Sprintf("sessions/%s/.claude", name), ReadOnly: false},
// Mount runner token secret as volume for dynamic token refresh
{Name: "runner-token", MountPath: "/app/runner-token", ReadOnly: true},
},

Env: func() []corev1.EnvVar {
Expand Down Expand Up @@ -1153,26 +1174,12 @@ func handleAgenticSessionEvent(obj *unstructured.Unstructured) error {
base = append(base, corev1.EnvVar{Name: "PARENT_SESSION_ID", Value: parentSessionID})
log.Printf("Session %s: passing PARENT_SESSION_ID=%s to runner", name, parentSessionID)
}
// If backend annotated the session with a runner token secret, inject only BOT_TOKEN
// Secret contains: 'k8s-token' (for CR updates)
// Prefer annotated secret name; fallback to deterministic name
secretName := ""
if meta, ok := currentObj.Object["metadata"].(map[string]interface{}); ok {
if anns, ok := meta["annotations"].(map[string]interface{}); ok {
if v, ok := anns["ambient-code.io/runner-token-secret"].(string); ok && strings.TrimSpace(v) != "" {
secretName = strings.TrimSpace(v)
}
}
}
if secretName == "" {
secretName = fmt.Sprintf("ambient-runner-token-%s", name)
}
Comment on lines -1158 to -1169
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude any risks to deleting this code?

// Inject BOT_TOKEN_PATH pointing to mounted secret volume
// Token is mounted from runnerTokenSecretName at /app/runner-token
// This allows the runner to read refreshed tokens without pod restart
base = append(base, corev1.EnvVar{
Name: "BOT_TOKEN",
ValueFrom: &corev1.EnvVarSource{SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
Key: "k8s-token",
}},
Name: "BOT_TOKEN_PATH",
Value: "/app/runner-token/k8s-token",
})
// Add CR-provided envs last (override base when same key)
if spec, ok := currentObj.Object["spec"].(map[string]interface{}); ok {
Expand Down
24 changes: 21 additions & 3 deletions components/runners/runner-shell/runner_shell/core/transport_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,20 @@ async def connect(self):
"""Connect to WebSocket endpoint."""
try:
# Forward Authorization header if BOT_TOKEN (runner SA token) is present
# Read from file if BOT_TOKEN_PATH is set (for dynamic token refresh)
# Otherwise fall back to BOT_TOKEN env var (backward compatibility)
headers: Dict[str, str] = {}
token = (os.getenv("BOT_TOKEN") or "").strip()
token = ""
token_path = (os.getenv("BOT_TOKEN_PATH") or "").strip()
if token_path:
try:
with open(token_path, "r", encoding="utf-8") as f:
token = f.read().strip()
logger.info(f"Read token from {token_path}")
except Exception as e:
logger.warning(f"Failed to read token from {token_path}: {e}")
if not token:
token = (os.getenv("BOT_TOKEN") or "").strip()
if token:
headers["Authorization"] = f"Bearer {token}"

Expand Down Expand Up @@ -69,10 +81,16 @@ async def connect(self):
)
# Surface a clearer hint when auth is likely missing
if status == 401:
token_path = (os.getenv("BOT_TOKEN_PATH") or "").strip()
has_token = bool((os.getenv("BOT_TOKEN") or "").strip())
if not has_token:
has_token_path = bool(token_path)
if not has_token and not has_token_path:
logger.error(
"No BOT_TOKEN present; backend project routes require Authorization."
"No BOT_TOKEN or BOT_TOKEN_PATH present; backend project routes require Authorization."
)
elif has_token_path and not token:
logger.error(
f"BOT_TOKEN_PATH is set to {token_path} but token could not be read."
)
raise
except Exception as e:
Expand Down
Loading