Skip to content

Commit d5715d0

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 89a07cd commit d5715d0

File tree

16 files changed

+1341
-289
lines changed

16 files changed

+1341
-289
lines changed

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ build-runner: ## Build the Claude Code runner container image
6262
cd components/runners && $(CONTAINER_ENGINE) build $(PLATFORM_FLAG) $(BUILD_FLAGS) -t $(RUNNER_IMAGE) -f claude-code-runner/Dockerfile .
6363

6464
# Kubernetes deployment
65-
deploy: ## Deploy all components (auto-detects OpenShift or Kubernetes)
66-
@echo "Deploying to cluster..."
65+
deploy: ## Deploy all components to OpenShift (production overlay)
66+
@echo "Deploying to OpenShift..."
6767
cd components/manifests && ./deploy.sh
6868

6969
# Cleanup
70-
clean: ## Clean up all Kubernetes resources
70+
clean: ## Clean up all Kubernetes resources (production overlay)
7171
@echo "Cleaning up Kubernetes resources..."
7272
cd components/manifests && ./deploy.sh clean
7373

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ spec:
5353
type: integer
5454
description: "Index of the repo in repos array treated as the main repo (Claude working dir). Defaults to 0 (first repo)."
5555
default: 0
56+
workspaceImage:
57+
type: string
58+
description: "Container image for the workspace environment. Agent will execute commands in this container via kubectl exec. Examples: python:3.11, node:20, rust:1.75. If not specified, the agent runs commands directly (legacy mode)."
5659
interactive:
5760
type: boolean
5861
description: "When true, run session in interactive chat mode using inbox/outbox files"

components/manifests/deploy.sh

Lines changed: 78 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/bin/bash
22

3-
# Deployment Script for vTeam Ambient Agentic Runner
4-
# Supports both OpenShift and standard Kubernetes (kind, etc.)
3+
# OpenShift Deployment Script for vTeam Ambient Agentic Runner
54
# Usage: ./deploy.sh
65
# Or with environment variables: NAMESPACE=my-namespace ./deploy.sh
76
# Note: This script deploys pre-built images. Build and push images first.
@@ -31,27 +30,6 @@ command_exists() {
3130
command -v "$1" >/dev/null 2>&1
3231
}
3332

34-
# Detect platform (OpenShift vs standard Kubernetes)
35-
detect_platform() {
36-
if command_exists oc && oc api-resources | grep -q "route.openshift.io"; then
37-
echo "openshift"
38-
elif command_exists kubectl; then
39-
echo "kubernetes"
40-
else
41-
echo "unknown"
42-
fi
43-
}
44-
45-
# Get the appropriate CLI command
46-
get_cli_cmd() {
47-
local platform=$1
48-
if [ "$platform" = "openshift" ]; then
49-
echo "oc"
50-
else
51-
echo "kubectl"
52-
fi
53-
}
54-
5533
# Helper: Run the OAuth setup (Route host, OAuthClient, Secret)
5634
oauth_setup() {
5735
echo -e "${YELLOW}Configuring OpenShift OAuth for the frontend...${NC}"
@@ -146,18 +124,6 @@ EOF
146124
oc -n ${NAMESPACE} rollout restart deployment/frontend
147125
}
148126

149-
# Detect platform
150-
PLATFORM=$(detect_platform)
151-
if [ "$PLATFORM" = "unknown" ]; then
152-
echo -e "${RED}❌ Neither oc nor kubectl found. Please install one.${NC}"
153-
exit 1
154-
fi
155-
CLI_CMD=$(get_cli_cmd "$PLATFORM")
156-
157-
echo -e "${BLUE}Detected platform: ${GREEN}${PLATFORM}${NC}"
158-
echo -e "${BLUE}Using CLI: ${GREEN}${CLI_CMD}${NC}"
159-
echo ""
160-
161127
# Configuration
162128
NAMESPACE="${NAMESPACE:-ambient-code}"
163129
# Allow overriding images via CONTAINER_REGISTRY/IMAGE_TAG or explicit DEFAULT_*_IMAGE
@@ -170,13 +136,6 @@ DEFAULT_RUNNER_IMAGE="${DEFAULT_RUNNER_IMAGE:-${CONTAINER_REGISTRY}/vteam_claude
170136
# Content service image (defaults to same as backend, but can be overridden)
171137
CONTENT_SERVICE_IMAGE="${CONTENT_SERVICE_IMAGE:-${DEFAULT_BACKEND_IMAGE}}"
172138

173-
# Select overlay based on platform
174-
if [ "$PLATFORM" = "openshift" ]; then
175-
OVERLAY="production"
176-
else
177-
OVERLAY="kubernetes"
178-
fi
179-
180139
# Handle uninstall/clean command early
181140
if [ "${1:-}" = "uninstall" ] || [ "${1:-}" = "clean" ]; then
182141
echo -e "${YELLOW}Uninstalling vTeam from namespace ${NAMESPACE}...${NC}"
@@ -267,10 +226,8 @@ EOF
267226
exit 0
268227
fi
269228

270-
echo -e "${BLUE}🚀 vTeam Ambient Agentic Runner Deployment${NC}"
271-
echo -e "${BLUE}===========================================${NC}"
272-
echo -e "Platform: ${GREEN}${PLATFORM}${NC}"
273-
echo -e "Overlay: ${GREEN}${OVERLAY}${NC}"
229+
echo -e "${BLUE}🚀 vTeam Ambient Agentic Runner - OpenShift Deployment${NC}"
230+
echo -e "${BLUE}====================================================${NC}"
274231
echo -e "Namespace: ${GREEN}${NAMESPACE}${NC}"
275232
echo -e "Backend Image: ${GREEN}${DEFAULT_BACKEND_IMAGE}${NC}"
276233
echo -e "Frontend Image: ${GREEN}${DEFAULT_FRONTEND_IMAGE}${NC}"
@@ -281,6 +238,11 @@ echo ""
281238

282239
# Check prerequisites
283240
echo -e "${YELLOW}Checking prerequisites...${NC}"
241+
if ! command_exists oc; then
242+
echo -e "${RED}❌ OpenShift CLI (oc) not found. Please install it first.${NC}"
243+
exit 1
244+
fi
245+
284246
if ! command_exists kustomize; then
285247
echo -e "${RED}❌ Kustomize not found. Please install it first.${NC}"
286248
exit 1
@@ -289,50 +251,47 @@ fi
289251
echo -e "${GREEN}✅ Prerequisites check passed${NC}"
290252
echo ""
291253

292-
# Check if logged in
293-
echo -e "${YELLOW}Checking cluster authentication...${NC}"
294-
if ! $CLI_CMD cluster-info >/dev/null 2>&1; then
295-
echo -e "${RED}❌ Not connected to cluster. Please configure kubectl/oc first.${NC}"
254+
# Check if logged in to OpenShift
255+
echo -e "${YELLOW}Checking OpenShift authentication...${NC}"
256+
if ! oc whoami >/dev/null 2>&1; then
257+
echo -e "${RED}❌ Not logged in to OpenShift. Please run 'oc login' first.${NC}"
296258
exit 1
297259
fi
298260

299-
CURRENT_USER=$($CLI_CMD auth whoami 2>/dev/null || echo "unknown")
300-
echo -e "${GREEN}✅ Authenticated as: ${CURRENT_USER}${NC}"
261+
echo -e "${GREEN}✅ Authenticated as: $(oc whoami)${NC}"
301262
echo ""
302263

303-
# Prepare oauth secret env file for kustomize secretGenerator (OpenShift only)
304-
if [ "$PLATFORM" = "openshift" ]; then
305-
echo -e "${YELLOW}Preparing oauth secret env for kustomize...${NC}"
306-
OAUTH_ENV_FILE="oauth-secret.env"
307-
CLIENT_SECRET_VALUE="${OCP_OAUTH_CLIENT_SECRET:-}"
308-
COOKIE_SECRET_VALUE="${OCP_OAUTH_COOKIE_SECRET:-}"
309-
if [[ -z "$CLIENT_SECRET_VALUE" ]]; then
310-
CLIENT_SECRET_VALUE=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
311-
fi
312-
# cookie_secret must be exactly 16, 24, or 32 bytes. Use 32 ASCII bytes by default.
313-
if [[ -z "$COOKIE_SECRET_VALUE" ]]; then
314-
COOKIE_SECRET_VALUE=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
315-
fi
316-
# If provided via .env, ensure it meets required length
317-
COOKIE_LEN=${#COOKIE_SECRET_VALUE}
318-
if [[ $COOKIE_LEN -ne 16 && $COOKIE_LEN -ne 24 && $COOKIE_LEN -ne 32 ]]; then
319-
echo -e "${YELLOW}Provided OCP_OAUTH_COOKIE_SECRET length ($COOKIE_LEN) is invalid; regenerating 32-byte value...${NC}"
320-
COOKIE_SECRET_VALUE=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
321-
fi
322-
cat > "$OAUTH_ENV_FILE" << EOF
264+
# Prepare oauth secret env file for kustomize secretGenerator
265+
echo -e "${YELLOW}Preparing oauth secret env for kustomize...${NC}"
266+
OAUTH_ENV_FILE="oauth-secret.env"
267+
CLIENT_SECRET_VALUE="${OCP_OAUTH_CLIENT_SECRET:-}"
268+
COOKIE_SECRET_VALUE="${OCP_OAUTH_COOKIE_SECRET:-}"
269+
if [[ -z "$CLIENT_SECRET_VALUE" ]]; then
270+
CLIENT_SECRET_VALUE=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
271+
fi
272+
# cookie_secret must be exactly 16, 24, or 32 bytes. Use 32 ASCII bytes by default.
273+
if [[ -z "$COOKIE_SECRET_VALUE" ]]; then
274+
COOKIE_SECRET_VALUE=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
275+
fi
276+
# If provided via .env, ensure it meets required length
277+
COOKIE_LEN=${#COOKIE_SECRET_VALUE}
278+
if [[ $COOKIE_LEN -ne 16 && $COOKIE_LEN -ne 24 && $COOKIE_LEN -ne 32 ]]; then
279+
echo -e "${YELLOW}Provided OCP_OAUTH_COOKIE_SECRET length ($COOKIE_LEN) is invalid; regenerating 32-byte value...${NC}"
280+
COOKIE_SECRET_VALUE=$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
281+
fi
282+
cat > "$OAUTH_ENV_FILE" << EOF
323283
client-secret=${CLIENT_SECRET_VALUE}
324284
cookie_secret=${COOKIE_SECRET_VALUE}
325285
EOF
326-
echo -e "${GREEN}✅ Generated ${OAUTH_ENV_FILE}${NC}"
327-
echo ""
328-
fi
286+
echo -e "${GREEN}✅ Generated ${OAUTH_ENV_FILE}${NC}"
287+
echo ""
329288

330289

331290
# Deploy using kustomize
332-
echo -e "${YELLOW}Deploying using Kustomize...${NC}"
291+
echo -e "${YELLOW}Deploying to OpenShift using Kustomize...${NC}"
333292

334-
# Use appropriate overlay
335-
cd overlays/${OVERLAY}
293+
# Use production overlay
294+
cd overlays/production
336295

337296
# Set namespace if different from default
338297
if [ "$NAMESPACE" != "ambient-code" ]; then
@@ -349,39 +308,35 @@ kustomize edit set image quay.io/ambient_code/vteam_claude_runner:latest=${DEFAU
349308

350309
# Build and apply manifests
351310
echo -e "${BLUE}Building and applying manifests...${NC}"
352-
kustomize build . | $CLI_CMD apply -f -
311+
kustomize build . | oc apply -f -
353312

354313
# Return to manifests root
355314
cd ../..
356315

357316
# Check if namespace exists and is active
358317
echo -e "${YELLOW}Checking namespace status...${NC}"
359-
if ! $CLI_CMD get namespace ${NAMESPACE} >/dev/null 2>&1; then
318+
if ! oc get namespace ${NAMESPACE} >/dev/null 2>&1; then
360319
echo -e "${RED}❌ Namespace ${NAMESPACE} does not exist${NC}"
361320
exit 1
362321
fi
363322

364323
# Check if namespace is active
365-
NAMESPACE_PHASE=$($CLI_CMD get namespace ${NAMESPACE} -o jsonpath='{.status.phase}')
324+
NAMESPACE_PHASE=$(oc get namespace ${NAMESPACE} -o jsonpath='{.status.phase}')
366325
if [ "$NAMESPACE_PHASE" != "Active" ]; then
367326
echo -e "${RED}❌ Namespace ${NAMESPACE} is not active (phase: ${NAMESPACE_PHASE})${NC}"
368327
exit 1
369328
fi
370329
echo -e "${GREEN}✅ Namespace ${NAMESPACE} is active${NC}"
371330

372-
# Switch to the target namespace (OpenShift only)
373-
if [ "$PLATFORM" = "openshift" ]; then
374-
echo -e "${BLUE}Switching to namespace ${NAMESPACE}...${NC}"
375-
oc project ${NAMESPACE}
376-
fi
331+
# Switch to the target namespace
332+
echo -e "${BLUE}Switching to namespace ${NAMESPACE}...${NC}"
333+
oc project ${NAMESPACE}
377334

378335
###############################################
379-
# OAuth setup: Route host, OAuthClient, Secret (OpenShift only)
336+
# OAuth setup: Route host, OAuthClient, Secret
380337
###############################################
381-
if [ "$PLATFORM" = "openshift" ]; then
382-
if ! oauth_setup; then
383-
echo -e "${YELLOW}OAuth setup completed with warnings/errors. You may need a cluster-admin to apply the OAuthClient.${NC}"
384-
fi
338+
if ! oauth_setup; then
339+
echo -e "${YELLOW}OAuth setup completed with warnings/errors. You may need a cluster-admin to apply the OAuthClient.${NC}"
385340
fi
386341

387342
# Apply git configuration if we created a patch
@@ -393,24 +348,24 @@ fi
393348

394349
# Update operator deployment with custom runner image
395350
echo -e "${BLUE}Updating operator with custom runner image...${NC}"
396-
$CLI_CMD patch deployment agentic-operator -n ${NAMESPACE} -p "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"agentic-operator\",\"env\":[{\"name\":\"AMBIENT_CODE_RUNNER_IMAGE\",\"value\":\"${DEFAULT_RUNNER_IMAGE}\"}]}]}}}}" --type=strategic
351+
oc patch deployment agentic-operator -n ${NAMESPACE} -p "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"agentic-operator\",\"env\":[{\"name\":\"AMBIENT_CODE_RUNNER_IMAGE\",\"value\":\"${DEFAULT_RUNNER_IMAGE}\"}]}]}}}}" --type=strategic
397352

398353
# Update backend deployment with content service image
399354
echo -e "${BLUE}Updating backend with content service image...${NC}"
400-
$CLI_CMD patch deployment backend-api -n ${NAMESPACE} -p "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"backend-api\",\"env\":[{\"name\":\"CONTENT_SERVICE_IMAGE\",\"value\":\"${CONTENT_SERVICE_IMAGE}\"},{\"name\":\"IMAGE_PULL_POLICY\",\"value\":\"Always\"}]}]}}}}" --type=strategic
355+
oc patch deployment backend-api -n ${NAMESPACE} -p "{\"spec\":{\"template\":{\"spec\":{\"containers\":[{\"name\":\"backend-api\",\"env\":[{\"name\":\"CONTENT_SERVICE_IMAGE\",\"value\":\"${CONTENT_SERVICE_IMAGE}\"},{\"name\":\"IMAGE_PULL_POLICY\",\"value\":\"Always\"}]}]}}}}" --type=strategic
401356

402357
echo ""
403358
echo -e "${GREEN}✅ Deployment completed!${NC}"
404359
echo ""
405360

406361
# Wait for deployments to be ready
407362
echo -e "${YELLOW}Waiting for deployments to be ready...${NC}"
408-
$CLI_CMD rollout status deployment/backend-api --namespace=${NAMESPACE} --timeout=300s
409-
$CLI_CMD rollout status deployment/agentic-operator --namespace=${NAMESPACE} --timeout=300s
410-
$CLI_CMD rollout status deployment/frontend --namespace=${NAMESPACE} --timeout=300s
363+
oc rollout status deployment/backend-api --namespace=${NAMESPACE} --timeout=300s
364+
oc rollout status deployment/agentic-operator --namespace=${NAMESPACE} --timeout=300s
365+
oc rollout status deployment/frontend --namespace=${NAMESPACE} --timeout=300s
411366

412-
# Get service and ingress/route information
413-
echo -e "${BLUE}Getting service information...${NC}"
367+
# Get service and route information
368+
echo -e "${BLUE}Getting service and route information...${NC}"
414369
echo ""
415370
echo -e "${GREEN}🎉 Deployment successful!${NC}"
416371
echo -e "${GREEN}========================${NC}"
@@ -419,71 +374,52 @@ echo ""
419374

420375
# Show pod status
421376
echo -e "${BLUE}Pod Status:${NC}"
422-
$CLI_CMD get pods -n ${NAMESPACE}
377+
oc get pods -n ${NAMESPACE}
423378
echo ""
424379

425-
# Show services
380+
# Show services and route
426381
echo -e "${BLUE}Services:${NC}"
427-
$CLI_CMD get services -n ${NAMESPACE}
382+
oc get services -n ${NAMESPACE}
428383
echo ""
429-
430-
# Show routes (OpenShift) or ingress (Kubernetes)
431-
if [ "$PLATFORM" = "openshift" ]; then
432-
echo -e "${BLUE}Routes:${NC}"
433-
oc get route -n ${NAMESPACE} || true
434-
if [[ -z "${ROUTE_NAME:-}" ]]; then
435-
if oc get route frontend-route -n ${NAMESPACE} >/dev/null 2>&1; then
436-
ROUTE_NAME="frontend-route"
437-
elif oc get route frontend -n ${NAMESPACE} >/dev/null 2>&1; then
438-
ROUTE_NAME="frontend"
439-
fi
384+
echo -e "${BLUE}Routes:${NC}"
385+
oc get route -n ${NAMESPACE} || true
386+
if [[ -z "${ROUTE_NAME:-}" ]]; then
387+
if oc get route frontend-route -n ${NAMESPACE} >/dev/null 2>&1; then
388+
ROUTE_NAME="frontend-route"
389+
elif oc get route frontend -n ${NAMESPACE} >/dev/null 2>&1; then
390+
ROUTE_NAME="frontend"
440391
fi
441-
ROUTE_HOST=$(oc get route ${ROUTE_NAME:-frontend-route} -n ${NAMESPACE} -o jsonpath='{.spec.host}' 2>/dev/null || true)
442-
else
443-
echo -e "${BLUE}Ingress:${NC}"
444-
$CLI_CMD get ingress -n ${NAMESPACE} || true
445-
INGRESS_HOST=$($CLI_CMD get ingress frontend-ingress -n ${NAMESPACE} -o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "vteam.local")
446392
fi
393+
ROUTE_HOST=$(oc get route ${ROUTE_NAME:-frontend-route} -n ${NAMESPACE} -o jsonpath='{.spec.host}' 2>/dev/null || true)
447394
echo ""
448395

449-
# Cleanup generated files (OpenShift only)
450-
if [ "$PLATFORM" = "openshift" ]; then
451-
echo -e "${BLUE}Cleaning up generated files...${NC}"
452-
rm -f "$OAUTH_ENV_FILE"
453-
fi
396+
# Cleanup generated files
397+
echo -e "${BLUE}Cleaning up generated files...${NC}"
398+
rm -f "$OAUTH_ENV_FILE"
454399

455400
echo -e "${YELLOW}Next steps:${NC}"
456-
if [ "$PLATFORM" = "openshift" ]; then
457-
if [[ -n "${ROUTE_HOST}" ]]; then
458-
echo -e "1. Access the frontend via Route:"
459-
echo -e " ${BLUE}https://${ROUTE_HOST}${NC}"
460-
else
461-
echo -e "1. Access the frontend (fallback via port-forward):"
462-
echo -e " ${BLUE}oc port-forward svc/frontend-service 3000:3000 -n ${NAMESPACE}${NC}"
463-
echo -e " Then open: http://localhost:3000"
464-
fi
401+
if [[ -n "${ROUTE_HOST}" ]]; then
402+
echo -e "1. Access the frontend via Route:"
403+
echo -e " ${BLUE}https://${ROUTE_HOST}${NC}"
465404
else
466-
echo -e "1. Access the frontend via Ingress:"
467-
echo -e " ${BLUE}http://${INGRESS_HOST}${NC}"
468-
echo -e " (Add '127.0.0.1 ${INGRESS_HOST}' to /etc/hosts for local access)"
469-
echo -e " Or via port-forward:"
470-
echo -e " ${BLUE}kubectl port-forward svc/frontend-service 3000:3000 -n ${NAMESPACE}${NC}"
405+
echo -e "1. Access the frontend (fallback via port-forward):"
406+
echo -e " ${BLUE}oc port-forward svc/frontend-service 3000:3000 -n ${NAMESPACE}${NC}"
471407
echo -e " Then open: http://localhost:3000"
472408
fi
473409
echo -e "2. Configure secrets in the UI (Runner/API keys, project settings)."
474410
echo -e " Open the app and follow Settings → Runner Secrets."
475411
echo -e "3. Monitor the deployment:"
476-
echo -e " ${BLUE}${CLI_CMD} get pods -n ${NAMESPACE} -w${NC}"
412+
echo -e " ${BLUE}oc get pods -n ${NAMESPACE} -w${NC}"
477413
echo -e "4. View logs:"
478-
echo -e " ${BLUE}${CLI_CMD} logs -f deployment/backend-api -n ${NAMESPACE}${NC}"
479-
echo -e " ${BLUE}${CLI_CMD} logs -f deployment/agentic-operator -n ${NAMESPACE}${NC}"
480-
echo -e "5. Monitor sessions:"
481-
echo -e " ${BLUE}${CLI_CMD} get agenticsessions -n ${NAMESPACE}${NC}"
414+
echo -e " ${BLUE}oc logs -f deployment/backend-api -n ${NAMESPACE}${NC}"
415+
echo -e " ${BLUE}oc logs -f deployment/agentic-operator -n ${NAMESPACE}${NC}"
416+
echo -e "4. Monitor RFE workflows:"
417+
echo -e " ${BLUE}oc get agenticsessions -n ${NAMESPACE}${NC}"
482418
echo ""
483419

484420
# Restore kustomization if we modified it
485421
echo -e "${BLUE}Restoring kustomization defaults...${NC}"
486-
cd overlays/${OVERLAY}
422+
cd overlays/production
487423
if [ "$NAMESPACE" != "ambient-code" ]; then
488424
kustomize edit set namespace ambient-code
489425
fi

0 commit comments

Comments
 (0)