# Agent manifest — `/.well-known/agent.json` > Normative requirements: [SPEC.md §6](../SPEC.md#6-agent-manifest-must) The agent-gateway discovers agents by fetching `GET {AGENT_SELF_URL}/.well-known/agent.json` after a successful `POST /agents/register-url` (see [registration.md](registration.md)). ## Schema ```jsonc { "name": "Policy Transformer", // human-readable, used as dedup key "version": "1.0.0", // semver, MUST match .image-version "description": "...", // 1-3 sentences "url": "/", // base path, almost always "/" "skills": [ // see "Skills" below { "id": "policy_generation", // snake_case, stable identifier "name": "Policy Generation", // human-readable "description": "...", // 1-2 sentences "tags": ["policy", "generate"], // free-form filters "examples": [ // example natural-language prompts "Generate a Kyverno policy that requires labels" ] } ], "capabilities": { "streaming": false, // SSE/WebSocket support "conversational": false, // multi-turn chat support "direct_api": true // synchronous request/response } } ``` ## Skills Skills declare what the agent can do. They are surfaced to: - The Backstage agent catalogue page, - The agent-gateway's discovery API (so other agents can route by capability), - LLM tool registries (so the platform-orchestrator can pick a skill at runtime). ### Defining skills with `a2a-sdk` Most agents use the `a2a-sdk`'s `AgentSkill` type: ```python from a2a.types import AgentSkill POLICY_TRANSFORMER_SKILLS = [ AgentSkill( id="policy_generation", name="Policy Generation", description=( "Generate production-ready Kyverno, Azure Policy, or " "OPA/Gatekeeper policies from a natural-language description." ), tags=["policy", "generate", "kyverno", "azure", "gatekeeper", "opa"], examples=[ "Generate a Kyverno policy that requires all pods to have app and owner labels", ], ), # ... ] ``` ### Defensive serialisation `AgentSkill` is currently exposed as a **protobuf `Message`** in `a2a-sdk` runtime images, even though older type stubs suggest pydantic. Calling `s.model_dump()` blindly raises `AttributeError: model_dump` and the manifest endpoint returns 500 — which then causes the gateway's `register-url` call to fail with `HTTP 400 — Failed to fetch agent card ... HTTP 500`. The agent **MUST** serialise skills with a defensive helper: ```python def _skill_dict(s) -> dict: if hasattr(s, "model_dump"): return s.model_dump() if hasattr(s, "DESCRIPTOR"): try: from google.protobuf.json_format import MessageToDict return MessageToDict(s, preserving_proto_field_name=True) except Exception: pass if hasattr(s, "dict") and callable(getattr(s, "dict")): try: return s.dict() except Exception: pass return { "id": getattr(s, "id", None), "name": getattr(s, "name", None), "description": getattr(s, "description", ""), "tags": list(getattr(s, "tags", []) or []), "examples": list(getattr(s, "examples", []) or []), } ``` The canonical copy lives in [`../template/app/agent.py`](../template/app/agent.py). ## The route handler ```python async def agent_manifest(request: Request) -> JSONResponse: """GET /.well-known/agent.json — discovery manifest consumed by agent-gateway.""" return JSONResponse({ "name": AGENT_CONFIG["name"], "version": AGENT_CONFIG["version"], "description": AGENT_CONFIG["description"], "url": "/", "skills": [_skill_dict(s) for s in AGENT_SKILLS], "capabilities": AGENT_CONFIG["capabilities"], }) ``` ## Capability flags | Flag | Meaning | Set to `true` when… | |---|---|---| | `streaming` | The agent supports server-sent events on at least one route. | You expose `text/event-stream` responses. | | `conversational` | Multi-turn chat with session state. | You hold `session_id` → state in memory or Redis. | | `direct_api` | Synchronous request → response. | The default for most agents. | These are advisory — the gateway uses them to pick UI affordances, not to gate routing. ## Naming and dedup The gateway normalises `name` to lower-hyphen-case before storing the registration row. `Policy Transformer` and `policy-transformer` collide; pick one form and stick to it. The registration row's `name` becomes the path segment in `/proxy/agent-gateway/{agent_name}/{path}`. If two agents register with the same normalised name, the latest registration wins. Versioned agents (e.g. `Modernization Factory` vs `Modernization Factory v2`) **MUST** use distinct `name` strings. ## Validation A simple smoke test you can run inside the pod: ```bash kubectl exec deployment/ -c -- \ curl -fs http://localhost:/.well-known/agent.json | jq -e '.name and .version and (.skills|type=="array")' ``` Exit code 0 means the manifest is structurally valid. The full validation lives in the gateway and runs automatically on every `register-url` call.