The Claude Code Runner is a Python-based component that wraps the Claude Code SDK to provide agentic coding capabilities within the Ambient platform.
The runner follows the Ambient Runner SDK architecture (ADR-0006) with a layered design:
main.py- FastAPI entry point with AG-UI run/interrupt/health endpointsadapter.py- Platform adapter that configures and creates theClaudeAgentAdapterprompts.py- System prompt builder (workspace context, workflows, MCP instructions)auth.py- SDK authentication (API key, Vertex AI) and runtime credentialsworkspace.py- Workspace validation and path resolutionmcp.py- MCP server configuration and tool allowlistingobservability.py- Langfuse + optional MLflow tracing (observability_config.py,mlflow_observability.py,observability_privacy.py)context.py- Runner context for session and workspace managementsecurity_utils.py- Security utilities for sanitizing secrets and timeouts
repos.py-/repos/add,/repos/remove,/repos/statusworkflow.py-/workflow(runtime workflow switching)feedback.py-/feedback(Langfuse thumbs-up/down)capabilities.py-/capabilities(framework + platform features)mcp_status.py-/mcp/status(MCP server diagnostics)state.py- Shared mutable state for all endpoint routers
tracing.py- Langfuse tracing middleware (wraps AG-UI event stream)developer_events.py- Platform setup lifecycle events (role="developer")
Extracted package with the bridge pattern for framework-agnostic support:
bridge.py-PlatformBridgeABC,PlatformContext,FrameworkCapabilitiesapp.py-add_ambient_endpoints(app, bridge)public APIbridges/claude.py-ClaudeBridge(Claude Agent SDK)bridges/langgraph.py-LangGraphBridge(validates abstraction)
The Claude Code Runner uses a hybrid system prompt approach that combines:
- Base Claude Code Prompt - The built-in
claude_codesystem prompt from the Claude Agent SDK - Workspace Context - Custom workspace-specific information appended to the base prompt
In prompts.py, the system prompt uses SystemPromptPreset format via build_sdk_system_prompt():
system_prompt_config = {
"type": "preset",
"preset": "claude_code",
"append": workspace_prompt,
}This configuration ensures that:
- Claude receives the standard Claude Code instructions and capabilities
- Additional workspace context is provided, including:
- Repository structure and locations
- Active workflow information
- Artifacts and file upload locations
- Git branch and push instructions for auto-push repos
- MCP integration setup instructions
- Workflow-specific instructions from
ambient.json
The workspace context prompt is built by build_sdk_system_prompt() in prompts.py and includes:
- Working Directory: Current workflow or repository location
- Artifacts Path: Where to create output files
- Uploaded Files: Files uploaded by the user
- Repositories: List of repos available in the session
- Working Branch: Feature branch for all repos (e.g.,
ambient/<session-id>) - Git Push Instructions: Auto-push configuration for specific repos
- MCP Integrations: Instructions for setting up Google Drive and Jira access
- Workflow Instructions: Custom system prompt from workflow's
ambient.json
ANTHROPIC_API_KEY- Anthropic API key for Claude accessUSE_VERTEX- Set to1to use Vertex AI instead of direct API keys (unified flag for all runners; legacyCLAUDE_CODE_USE_VERTEXandGEMINI_USE_VERTEXstill accepted)GOOGLE_APPLICATION_CREDENTIALS- Path to GCP service account key (for Vertex AI)ANTHROPIC_VERTEX_PROJECT_ID- GCP project ID (for Vertex AI)CLOUD_ML_REGION- GCP region (for Vertex AI)
LLM_MODEL- Model to use (e.g.,claude-sonnet-4-5)LLM_MAX_TOKENS/MAX_TOKENS- Maximum tokens per responseLLM_TEMPERATURE/TEMPERATURE- Temperature for sampling
AGENTIC_SESSION_NAME- Session name/IDAGENTIC_SESSION_NAMESPACE- K8s namespace for the sessionIS_RESUME- Set totruewhen resuming a sessionINITIAL_PROMPT- Initial user prompt
REPOS_JSON- JSON array of repository configurations[ { "url": "https://github.com/owner/repo", "name": "repo-name", "branch": "ambient/session-id", "autoPush": true } ]MAIN_REPO_NAME- Name of the main repository (CWD)MAIN_REPO_INDEX- Index of main repo (if name not specified)
ACTIVE_WORKFLOW_GIT_URL- URL of active workflow repository
Langfuse (unchanged defaults when OBSERVABILITY_BACKENDS is unset — Langfuse-only):
LANGFUSE_ENABLED- Enable Langfuse (true/1)LANGFUSE_PUBLIC_KEY- Langfuse public keyLANGFUSE_SECRET_KEY- Langfuse secret keyLANGFUSE_HOST- Langfuse host URLLANGFUSE_MASK_MESSAGES- Redact message bodies (truedefault; shared with MLflow path)
Backend selection
OBSERVABILITY_BACKENDS- Comma-separated:langfuse,mlflow, or both (e.g.langfuse,mlflow). If unset, defaults tolangfuseonly so existing Langfuse behaviour is preserved.- Turn traces are named
llm_interaction(vendor-neutral).RUNNER_TYPE(same values as bridge selection:claude-agent-sdk,gemini-cli, …) is added to Langfuse tags (runner:<type>) and to span metadata for MLflow / Langfuse.
MLflow GenAI tracing (optional extra: pip install 'ambient-runner[mlflow-observability]' — pins mlflow[kubernetes]>=3.11 for cluster auth)
MLFLOW_TRACING_ENABLED- Must betrue/1together withmlflowinOBSERVABILITY_BACKENDSMLFLOW_TRACKING_URI- MLflow tracking server URI (e.g.https://mlflow.example.comorfile:./mlrunsfor local tests)MLFLOW_EXPERIMENT_NAME- Experiment name (default:ambient-code-sessions)MLFLOW_TRACKING_AUTH- Optional. Set tokubernetes-namespacedon OpenShift AI / Kubeflow-style MLflow so the client addsAuthorization: Bearer <SA JWT>andX-MLFLOW-WORKSPACE(from the pod namespace). Requires thekubernetesPython extra (included viamlflow[kubernetes]). The 3.11 client is expected to work against typical 3.9–3.10 era tracking servers; align versions with your platform if you hit protocol issues.MLFLOW_WORKSPACE- Optional. Overrides the workspace sent asX-MLFLOW-WORKSPACEwhen using Kubernetes auth (otherwise the namespace file is used).
On the cluster, when the operator copies ambient-admin-mlflow-observability-secret, it runs the runner pod as ambient-session-<session> with service account token automount enabled so MLflow can read /var/run/secrets/kubernetes.io/serviceaccount/{token,namespace}. The session Role includes get / list / update on experiments in API group mlflow.kubeflow.org (adjust RBAC if your MLflow operator uses a different API group).
OTLP export from MLflow (no code changes — configure before the process creates spans; see MLflow OTLP export):
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT- e.g.http://otel-collector:4318/v1/traceswithopentelemetry-exporter-otlpinstalledOTEL_SERVICE_NAME- Service name on exported spansMLFLOW_TRACE_ENABLE_OTLP_DUAL_EXPORT=true- Send traces to both the MLflow Tracking Server and OTLP- Optional:
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,OTEL_EXPORTER_OTLP_TRACES_HEADERS
BACKEND_API_URL- URL of the backend APIPROJECT_NAME- Project nameBOT_TOKEN- Authentication token for backend API callsUSER_ID- User ID for observabilityUSER_NAME- User name for observability
MCP_CONFIG_FILE- Path to MCP servers config (default:/app/claude-runner/.mcp.json)
USER_GOOGLE_EMAIL- User's Google email for authentication (set by operator)GOOGLE_MCP_CREDENTIALS_DIR- Path to Google OAuth credentials directory (default:/workspace/.google_workspace_mcp/credentials)
The runner supports MCP (Model Context Protocol) servers for extending Claude's capabilities:
- webfetch - Fetch content from URLs
- mcp-atlassian - Jira integration for issue management
- google-workspace - Google Drive integration for file access
- session - Session control tools (restart_session)
MCP servers are configured in .mcp.json and loaded at runtime.
The google-workspace MCP server (workspace-mcp) requires OAuth credentials for Google Drive access:
Credentials Flow:
- User authenticates via OAuth in the frontend (
/integrationspage) - Backend stores credentials in a cluster-level K8s secret
- Operator creates a session-specific secret with the user's credentials
- Secret is mounted read-only at
/app/.google_workspace_mcp/credentials/ - A postStart lifecycle hook copies credentials to writable
/workspace/.google_workspace_mcp/credentials/ - The MCP server uses the writable path for token refresh
Credentials Format (flat JSON structure):
{
"token": "<access_token>",
"refresh_token": "<refresh_token>",
"token_uri": "https://oauth2.googleapis.com/token",
"client_id": "<oauth_client_id>",
"client_secret": "<oauth_client_secret>",
"scopes": ["https://www.googleapis.com/auth/drive.readonly"],
"expiry": "2026-01-30T12:00:00"
}Why writable credentials directory? The workspace-mcp server refreshes OAuth tokens automatically when they expire. This requires writing updated tokens to the credentials file. K8s secrets are mounted read-only, so the postStart hook copies credentials to a writable location.
# Install dependencies
uv pip install -e ".[dev]"
# Run all tests
pytest -v
# Run with coverage
pytest --cov=. --cov-report=term-missingSee tests/README.md for detailed testing documentation.
# Install in development mode
uv pip install -e .
# Run the server
python main.pyThe runner exposes a FastAPI server with the following endpoints:
-
POST /run- Execute a run with Claude Code SDK- Request:
RunAgentInput(thread_id, run_id, messages) - Response: Server-Sent Events (SSE) stream of AG-UI protocol events
- Request:
-
POST /interrupt- Interrupt the active execution -
GET /health- Health check endpoint
The runner emits AG-UI protocol events via SSE:
RUN_STARTED- Run has startedTEXT_MESSAGE_START- Message started (user or assistant)TEXT_MESSAGE_CONTENT- Message content chunkTEXT_MESSAGE_END- Message completedTOOL_CALL_START- Tool invocation startedTOOL_CALL_ARGS- Tool argumentsTOOL_CALL_END- Tool invocation completedSTEP_STARTED- Processing step startedSTEP_FINISHED- Processing step completedSTATE_DELTA- State update (e.g., result payload)RAW- Custom events (thinking blocks, system logs, etc.)RUN_FINISHED- Run completedRUN_ERROR- Error occurred
The runner operates within a workspace at /workspace/ with the following structure:
/workspace/
├── repos/ # Cloned repositories
│ └── {repo-name}/ # Individual repository
├── workflows/ # Workflow repositories
│ └── {workflow-name}/ # Individual workflow
├── artifacts/ # Output files created by Claude
├── file-uploads/ # User-uploaded files
└── .google_workspace_mcp/ # Google OAuth credentials (writable copy)
└── credentials/
└── credentials.json
/app/
└── .claude/ # Claude SDK state (conversation history)
The runner container runs as uid=1001 (non-root). The init container (hydrate.sh) sets up directory permissions:
/workspace/artifacts,/workspace/file-uploads,/workspace/repos- 777 permissions- Required because MCP servers spawn as subprocesses and need write access to their working directory
chown 1001:0is attempted first but may fail on SELinux-restricted hosts
/app/.claude- 777 permissions- Claude SDK requires write access for conversation state
/workspace/.google_workspace_mcp/credentials/- 777 permissions- Created by postStart hook for writable OAuth token storage
The runner implements several security measures:
- Secret Sanitization: API keys and tokens are redacted from logs
- Timeout Protection: Operations have configurable timeouts
- User Context Validation: User IDs and names are sanitized
- Read-only Workflow Directories: Workflows are read-only, outputs go to artifacts
- OAuth Credentials Isolation: Google credentials are stored in session-specific secrets, copied to writable storage only within the container
See security_utils.py for implementation details.
Changed the system prompt configuration to use the SystemPromptPreset format:
Before (incorrect - caused SDK initialization error):
system_prompt_config = [
"claude_code",
{"type": "text", "text": workspace_prompt}
]After (correct SystemPromptPreset format):
system_prompt_config = {
"type": "preset",
"preset": "claude_code",
"append": workspace_prompt,
}Rationale:
ClaudeAgentOptions.system_promptexpectsstr | SystemPromptPreset | None- The list format was invalid and caused
'list' object has no attribute 'get'error SystemPromptPresetusestype="preset",preset="claude_code", and optionalappend- This leverages the built-in Claude Code system prompt with appended workspace context
Impact:
- Fixes SDK initialization failure
- Claude receives comprehensive instructions from both sources
- Better alignment with Claude Agent SDK type definitions
Files Changed:
components/runners/ambient-runner/adapter.py(lines 557-561)