Skip to content

The 7 Agents: Dockerfile, agent.json and Tools

Each agent runs as an independent A2A server on Cloud Run. To go from the local system to production, each one needs 3 new things: a Dockerfile, an agent.json, and tools that read context from the HTTP message instead of the filesystem.


FilePurpose
DockerfileBuilds the container image with ADK + A2A server
agent.jsonAgentCard — declares capabilities for A2A discovery
tools.pyRefactored tools with context_json parameter

All 7 agents use the same Dockerfile structure. The only thing that changes is the agent directory:

FROM python:3.11-slim
WORKDIR /app
# Install uv for fast dependency management
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# Copy agent code
COPY . .
# Install dependencies
RUN uv pip install --system -r requirements.txt
# ADK A2A server on port 8080 (Cloud Run default)
EXPOSE 8080
CMD ["python", "-m", "adk.a2a_server", "--port", "8080"]

Each agent publishes an agent.json at its root URL. This is the A2A standard for agent discovery — any client can read it to know what the agent does:

{
"name": "platform-architect",
"description": "Analyzes task requirements and decides the complete technology stack for the IDP",
"url": "https://platform-architect-HASH.run.app",
"version": "1.0.0",
"capabilities": {
"streaming": false,
"pushNotifications": false
},
"skills": [
{
"id": "analyze-stack",
"name": "Stack Analysis",
"description": "Analyzes requirements and produces platform-config.yaml with all architecture decisions"
}
]
}

The url field gets its final value after deploy — Cloud Run assigns a unique URL to each service.

In the local system, tools read from the shared filesystem. On Cloud Run, they receive context as a JSON string in the A2A message:

def get_platform_config() -> dict:
"""Reads platform-config.yaml from disk."""
config_path = os.path.join(OUTPUT_DIR, 'platform-config.yaml')
with open(config_path) as f:
return yaml.safe_load(f)
def get_platform_config(context_json: str = "") -> dict:
"""Reads context from A2A message. Falls back to disk if empty."""
if context_json:
return json.loads(context_json)
config_path = os.path.join(OUTPUT_DIR, 'platform-config.yaml')
with open(config_path) as f:
return yaml.safe_load(f)

The fallback to disk means the same code works both locally and on Cloud Run.


  • Input: Task description from the orchestrator
  • Output: platform-config.yaml — full stack decisions
  • Special: First in the chain, no previous context needed
  • Input: Task + Platform Architect context
  • Output: docker-compose/app-stack.yml with all services, healthchecks and volumes
  • Reads: platform-config.yaml from the accumulated context
  • Input: Task + PA + Infrastructure context
  • Output: security-report.json — vulnerabilities, scan results, APPROVED/BLOCKED
  • Reads: docker-compose/app-stack.yml from the accumulated context
  • Special: Can block the entire pipeline if it detects CRITICAL vulnerabilities
  • Input: Task + PA + Infra + Security context
  • Output: cicd/build.sh, test.sh, deploy.sh + Jenkinsfile
  • Reads: platform-config.yaml + security approval from context
  • Input: Task + PA + Infra + Sec + CICD context
  • Output: prometheus.yml + grafana-dashboards/ (app-metrics + system-metrics)
  • Reads: Infrastructure config for scrape targets
  • Input: Task + all 5 previous agents’ context
  • Output: cli-tool/idp — executable CLI with project-specific commands
  • Reads: Full stack config to generate coherent commands
  • Input: Task + all 6 previous agents’ context
  • Output: portal/ — complete FastAPI + HTMX web portal
  • Special: Most expensive agent (~45s) — reads everything to build a comprehensive portal

Next step: Deploy — 7 services on Cloud Run →