Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ build/
*.egg-info/
.idea/
.vscode/
runs/
runs/
49 changes: 23 additions & 26 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.
## The three primitives

```
┌───────────────────────────────────────────────────────────────────
│ developer ──HTTP──▶ session-router ──k8s patch──▶ agent pod
┌────────────────────────────────────────────────────────┐
│ developer ──HTTP──▶ sandbox-orchestrator ──claim──▶ agent pod │
│ (laptop / IDE) │ │ │
│ ▼ ▼ │
Redis + Cosmos Claude Code │
(session state) │ │
SandboxWarmPool (CRD) Claude Code │
(pre-warmed sandboxes) │ │
│ ▼ │
│ model-gateway │
│ (LiteLLM) │
Expand All @@ -39,8 +39,8 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.

| Primitive | What it is | Where it lives | Why |
|---|---|---|---|
| **Agent pod** | Ubuntu devcontainer w/ Claude Code installed via the official `ghcr.io/anthropics/devcontainer-features/claude-code:1.0` Feature | `containers/agent-pod/` + `charts/code-forge/templates/20-agent-pod.yaml` | Stateless, ephemeral, label-driven state machine: `warm → bound → cooldown` |
| **Session router** | Go service that maps `(dev_id, project_id) → free pod`, mints virtual keys, patches pod labels, scrubs idle | `containers/session-router/` + `charts/code-forge/templates/30-session-router.yaml` | The traffic cop. Stateless; state in Redis + Cosmos. Scales via KEDA HTTP Add-on (RPS) |
| **Agent sandbox** | Ubuntu devcontainer w/ Claude Code installed via the official `ghcr.io/anthropics/devcontainer-features/claude-code:1.0` Feature | `containers/agent-pod/` (image) + `charts/code-forge/templates/36-sandbox-template.yaml` (`SandboxTemplate`/`SandboxWarmPool`) | Stateless, ephemeral, pre-warmed in a pool; writable `/workspace` under a read-only root filesystem |
| **Sandbox orchestrator** | Python (FastAPI) service that provisions ONE agent sandbox per request — claims a warm pod, runs the command over the apiserver exec stream, tears it down | `containers/sandbox-orchestrator/` + `charts/code-forge/templates/35-sandbox-orchestrator.yaml` | The traffic cop. Stateless; relies on the agent-sandbox CRDs (`SandboxClaim`/`SandboxTemplate`/`SandboxWarmPool`) |
| **Model gateway** | LiteLLM proxy that fronts Foundry, holds an AAD token (refreshed via Workload Identity), enforces per-dev budgets | `containers/model-gateway/` + `charts/code-forge/templates/40-model-gateway.yaml` | Single audit/billing chokepoint. Agents never see Foundry directly |

## Repo map (start here)
Expand All @@ -60,8 +60,8 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.
│ └── templates/
│ ├── 00-namespaces.yaml
│ ├── 05-serviceaccounts.yaml
│ ├── 20-agent-pod.yaml
│ ├── 30-session-router.yaml
│ ├── 35-sandbox-orchestrator.yaml
│ ├── 36-sandbox-template.yaml
│ ├── 40-model-gateway.yaml
│ └── 50-network-policies.yaml
Expand All @@ -73,10 +73,11 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.
│ │ ├── agent-shutdown
│ │ ├── healthz.py
│ │ └── CLAUDE.md
│ ├── session-router/ ← Go (client-go + Redis)
│ │ ├── main.go
│ ├── sandbox-orchestrator/ ← Python (FastAPI + agent-sandbox SDK)
│ │ ├── sandbox_orchestrator/ ← api, manager, backends, config, models
│ │ ├── tests/
│ │ ├── Dockerfile
│ │ ├── go.mod
│ │ ├── pyproject.toml
│ │ └── CLAUDE.md
│ └── model-gateway/ ← LiteLLM + AAD-token sidecar
│ ├── Dockerfile
Expand All @@ -90,8 +91,6 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.
│ │ … servicebus, acr, aks, frontdoor, foundry
│ └── CLAUDE.md
├── deploy/ ← reference loose YAML (pre-Helm)
└── docs/
├── ARCHITECTURE.md ← deep-dive on every primitive
├── ONBOARDING.md ← 30-minute new-engineer ramp
Expand All @@ -104,21 +103,20 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.
## How a request flows (read this once, it makes everything click)

1. Developer runs `claude` in their IDE (or their CLI hits `https://api.codeforge.example.com`).
2. A thin client call goes to **session-router** with `{dev_id, project_id}`.
3. Router looks up Redis: existing warm session? → return pod handle.
No session? → list pods labeled `app=agent-pod,state=warm`, pick one, **patch labels** to `state=bound, dev-id=…, session-id=…`, attach the dev's workspace PVC, mint a per-session virtual key in LiteLLM, write session record to Cosmos.
4. Router returns the pod handle. Client streams I/O via `kubectl exec` (or an HTTP shim) into that pod.
5. Inside the pod, Claude Code calls the **model-gateway** (env: `ANTHROPIC_FOUNDRY_BASE_URL=http://model-gateway.platform.svc.cluster.local/anthropic`).
2. A thin client call goes to the **sandbox-orchestrator** with `{dev_id, command}`.
3. The orchestrator creates a `SandboxClaim` that **adopts a pre-warmed pod** from the `SandboxWarmPool` (sub-2s), instead of cold-starting. No free warm pod? → the claim provisions a fresh sandbox from the `SandboxTemplate`.
4. The orchestrator streams I/O into the sandbox via the kube-apiserver `exec` stream (the agent-pod image runs Claude Code, not an in-pod HTTP server).
5. Inside the sandbox, Claude Code calls the **model-gateway** (env: `ANTHROPIC_FOUNDRY_BASE_URL=http://model-gateway.platform.svc.cluster.local/anthropic`).
6. The gateway authenticates to **Foundry** with a federated AAD token, applies budget/RPM caps, forwards the request, logs cost.
7. Idle for 15 min → router patches pod `state=cooldown`, the pod's preStop hook scrubs `/workspace`, the ReplicaSet brings up a fresh `state=warm` replacement.
7. On completion (or TTL), the orchestrator deletes the `SandboxClaim`; the warm pool self-heals back to its target `readyReplicas`.

## Conventions

- **No static API keys.** Every credential is Azure Workload Identity (federated OIDC). If you're tempted to add `ANTHROPIC_API_KEY` to a secret, stop and read `docs/SECURITY.md`.
- **Pin model versions explicitly.** Aliases (`opus`, `sonnet`, `haiku`) auto-resolve on Foundry and break when Anthropic releases new models. We pin `claude-opus-4-8`, `claude-sonnet-4-6`, `claude-haiku-4-5` in `values.yaml`.
- **Pods are cattle.** Agent pods MUST be safe to nuke at any time. Anything durable goes in Redis (sessions), Cosmos (audit), or PVCs (per-dev workspaces).
- **Default-deny networking.** Agent pods can only reach the model-gateway and DNS — no public egress, no direct Foundry calls, no internal lateral movement.
- **Helm is the source of truth** for what's running. The loose YAML under `deploy/` is reference material; production deploys go through `make chart-install`.
- **Sandboxes are cattle.** Agent sandboxes MUST be safe to nuke at any time. Anything durable goes in the model-gateway audit log or per-dev PVCs.
- **Default-deny networking.** Agent sandboxes can only reach the model-gateway and DNS — no public egress, no direct Foundry calls, no internal lateral movement.
- **Helm is the source of truth** for what's running. Production deploys go through `make chart-install`.
- **Bicep is the source of truth** for what Azure resources exist.

## Common tasks
Expand All @@ -127,7 +125,7 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.
|---|---|
| Change agent-pod image | `containers/agent-pod/Dockerfile` + bump `agentPod.image.tag` |
| Add a model | `values.yaml` → `global.foundry.models` + LiteLLM ConfigMap |
| Tweak warm-pool size | `values.yaml` → `agentPod.replicas` (baseline) and `agentPod.keda.minReplicas/maxReplicas` |
| Tweak warm-pool size | `values.yaml` → `sandboxOrchestrator.sandbox.warmpoolReplicas` |
| Add a per-dev budget | `values.yaml` → `modelGateway.budgets` |
| Add a network egress allow | `templates/50-network-policies.yaml` |
| Rotate LiteLLM master key | `docs/OPERATIONS.md` → "Key rotation" |
Expand All @@ -140,8 +138,8 @@ rate-limits, audit, and a warm pool so cold-start is sub-2-seconds.
make chart-lint
make chart-template | head -50

# Compile router
cd containers/session-router && go build ./...
# Orchestrator unit tests
cd containers/sandbox-orchestrator && python -m pytest -q

# Dry-run image build (needs Docker daemon)
docker build -t code-forge/agent-pod:dev containers/agent-pod
Expand All @@ -152,7 +150,6 @@ docker build -t code-forge/agent-pod:dev containers/agent-pod
- Claude Code dev container: <https://code.claude.com/docs/en/devcontainer>
- Claude Code on Foundry: <https://code.claude.com/docs/en/microsoft-foundry>
- Claude Code LLM gateway: <https://code.claude.com/docs/en/llm-gateway>
- KEDA HTTP Add-on: <https://kedacore.github.io/http-add-on/>
- Azure Workload Identity: <https://azure.github.io/azure-workload-identity/docs/>

## Who runs this
Expand Down
57 changes: 35 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,68 @@
# Code Forge top-level Makefile — image build/push + chart install/template.
# =============================================================================
TAG ?= 1.0.0
ACR ?= acrtheclouds.azurecr.io
ACR ?= codeforgedemo.azurecr.io
RELEASE ?= code-forge
NAMESPACE ?= session-control
# AKS nodes are linux/amd64; build for that platform even on Apple Silicon.
PLATFORM ?= linux/amd64
# agent-sandbox controller + CRDs (core Sandbox + extensions: SandboxTemplate/
# SandboxWarmPool/SandboxClaim). https://github.com/kubernetes-sigs/agent-sandbox
AGENT_SANDBOX_VERSION ?= v0.4.6
AGENT_SANDBOX_BASE ?= https://github.com/kubernetes-sigs/agent-sandbox/releases/download/$(AGENT_SANDBOX_VERSION)

# Placeholder values used only for `helm lint` / `helm template` previews.
FOUNDRY_RESOURCE ?= demo
AZURE_TENANT_ID ?= 00000000-0000-0000-0000-000000000000
ORCHESTRATOR_CLIENT_ID ?= 11111111-1111-1111-1111-111111111111
GATEWAY_CLIENT_ID ?= 33333333-3333-3333-3333-333333333333
# Shared --set flags for the lint/template demo render.
DEMO_SET = \
--set global.foundry.resource=$(FOUNDRY_RESOURCE) \
--set global.azureTenantId=$(AZURE_TENANT_ID) \
--set workloadIdentity.sandboxOrchestrator.clientId=$(ORCHESTRATOR_CLIENT_ID) \
--set workloadIdentity.modelGateway.clientId=$(GATEWAY_CLIENT_ID)

.PHONY: help
help:
@echo "Targets:"
@echo " build-images # docker build agent-pod + session-router + model-gateway"
@echo " build-images # docker build agent-pod + sandbox-orchestrator + model-gateway"
@echo " push-images # docker push to \$$ACR"
@echo " chart-lint # helm lint"
@echo " chart-template # helm template (preview rendered yaml)"
@echo " chart-install # helm upgrade --install"
@echo " install-crds # kubectl apply agent-sandbox CRDs + controller"
@echo " chart-install # install-crds then helm upgrade --install"
@echo " chart-uninstall # helm uninstall"

.PHONY: build-images
build-images:
docker build -t $(ACR)/code-forge/agent-pod:$(TAG) containers/agent-pod
docker build -t $(ACR)/code-forge/session-router:$(TAG) containers/session-router
docker build -t $(ACR)/code-forge/model-gateway:$(TAG) containers/model-gateway
docker buildx build --platform $(PLATFORM) --load -t $(ACR)/code-forge/agent-pod:$(TAG) containers/agent-pod
docker buildx build --platform $(PLATFORM) --load -t $(ACR)/code-forge/model-gateway:$(TAG) containers/model-gateway
docker buildx build --platform $(PLATFORM) --load -t $(ACR)/code-forge/sandbox-orchestrator:$(TAG) containers/sandbox-orchestrator

.PHONY: push-images
push-images:
az acr login -n $(firstword $(subst ., ,$(ACR)))
docker push $(ACR)/code-forge/agent-pod:$(TAG)
docker push $(ACR)/code-forge/session-router:$(TAG)
docker push $(ACR)/code-forge/model-gateway:$(TAG)
docker push $(ACR)/code-forge/sandbox-orchestrator:$(TAG)

.PHONY: chart-lint
chart-lint:
helm lint charts/code-forge \
--set global.foundry.resource=demo \
--set global.azureTenantId=00000000-0000-0000-0000-000000000000 \
--set workloadIdentity.agentPod.clientId=11111111-1111-1111-1111-111111111111 \
--set workloadIdentity.sessionRouter.clientId=22222222-2222-2222-2222-222222222222 \
--set workloadIdentity.modelGateway.clientId=33333333-3333-3333-3333-333333333333 \
--set agentPod.keda.serviceBus.namespace=demo.servicebus.windows.net
helm lint charts/code-forge $(DEMO_SET)

.PHONY: chart-template
chart-template:
@helm template $(RELEASE) charts/code-forge \
--set global.foundry.resource=demo \
--set global.azureTenantId=00000000-0000-0000-0000-000000000000 \
--set workloadIdentity.agentPod.clientId=11111111-1111-1111-1111-111111111111 \
--set workloadIdentity.sessionRouter.clientId=22222222-2222-2222-2222-222222222222 \
--set workloadIdentity.modelGateway.clientId=33333333-3333-3333-3333-333333333333 \
--set agentPod.keda.serviceBus.namespace=demo.servicebus.windows.net
@helm template $(RELEASE) charts/code-forge $(DEMO_SET)

.PHONY: install-crds
install-crds:
kubectl apply --server-side -f $(AGENT_SANDBOX_BASE)/manifest.yaml
kubectl apply --server-side -f $(AGENT_SANDBOX_BASE)/extensions.yaml
kubectl -n agent-sandbox-system rollout status deploy/agent-sandbox-controller --timeout=120s

.PHONY: chart-install
chart-install:
chart-install: install-crds
helm upgrade --install $(RELEASE) charts/code-forge \
--create-namespace --namespace $(NAMESPACE) \
-f charts/code-forge/values-prod.yaml
Expand Down
Loading