Skip to content

Commit 97110ef

Browse files
cgwaltersclaude
andcommitted
feat(runner): experimental workspace container implementation
Prototype implementation of ADR-0006 workspace architecture: - MCP server for workspace command execution via kubectl exec - Tool replacement: disallow Bash, provide mcp__workspace__exec - Operator updates for workspace container pod spec - Integration and unit tests This is experimental and not yet ready for production use. Co-Authored-By: Claude <noreply@anthropic.com> Assisted-by: Claude Code (Opus 4.5)
1 parent ef0b1c4 commit 97110ef

File tree

8 files changed

+1249
-6
lines changed

8 files changed

+1249
-6
lines changed

components/manifests/base/crds/agenticsessions-crd.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ spec:
5656
interactive:
5757
type: boolean
5858
description: "When true, run session in interactive chat mode using inbox/outbox files"
59+
workspaceImage:
60+
type: string
61+
description: "Container image for the workspace environment. When specified, enables workspace container mode with separate agent and workspace containers."
5962
prompt:
6063
type: string
6164
description: "Optional initial prompt for the agentic session. If using a workflow with startupPrompt in ambient.json, this can be omitted."

components/operator/internal/handlers/sessions.go

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
15171650
var (
15181651
boolPtr = func(b bool) *bool { return &b }

components/runners/claude-code-runner/Dockerfile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ RUN apt-get update && apt-get install -y \
77
jq \
88
gh \
99
ca-certificates \
10-
&& rm -rf /var/lib/apt/lists/*
10+
&& rm -rf /var/lib/apt/lists/* \
11+
# Install kubectl for workspace container mode (MCP workspace exec)
12+
&& curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
13+
&& chmod +x kubectl \
14+
&& mv kubectl /usr/local/bin/
1115

1216
# Create working directory
1317
WORKDIR /app
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""MCP servers for Pattern B agent execution."""
2+
3+
from .workspace_exec import execute_command, exec_via_kubectl, exec_via_nsenter
4+
5+
__all__ = ["execute_command", "exec_via_kubectl", "exec_via_nsenter"]

0 commit comments

Comments
 (0)