Adds the normative contract every platform agent must conform to, plus a
working reference template under template/ that ticks every box out of
the box.
Spec is derived from the production agents shipped in
cjot-backstage-az/agents/ on branch sandbox/jonathan:
- agent-registry, agent-factory, decomposer, discovery-agent,
golden-path, modernization-factory, modernization-factory-v2,
policy-transformer, scaffold-agent, support-intake-agent, the-watcher
Contents:
- SPEC.md normative contract (13 sections + conformance checklist)
- CHANGELOG.md spec versioning (1.0.0)
- docs/
registration.md self-registration with the agent-gateway
observability.md OTel logs/metrics/traces wiring
manifest.md /.well-known/agent.json schema + AgentSkill
serialisation (pydantic vs protobuf)
kubernetes.md k8s deployment shape, mandatory cross-refs,
per-namespace agents, resource sizing
deployment.md .image-version, ACR build, deploy scripts, rollback
deviations.md tracked debts against the spec
- template/
.env.local.example, .gitignore, .image-version (0.1.0),
Dockerfile (python:3.12-slim, non-root UID 1001, HEALTHCHECK),
requirements.txt (harness floor + optional LLM stack),
app/agent.py (Starlette entry, lifespan + self-registration,
defensive _skill_dict for pydantic vs protobuf),
app/logging_setup.py (canonical OTel log bridge — copy verbatim),
app/metrics.py (meter + example counter/histogram),
app/skills.py (AGENT_CONFIG + AgentSkill list),
k8s/configmap.yaml + deployment.yaml (Deployment + Service,
OTel annotations + 6-var env block,
agents-sa + agents-kv-spc bindings),
scripts/deploy.sh (auto-bump + az acr build + apply + rollout),
scripts/full-deploy.sh (preflight + deploy + post-deploy smoke),
scripts/deploy-local.sh (docker/podman + .env.local)
3.8 KiB
Self-registration with the agent-gateway
Normative requirements: SPEC.md §5
Every agent self-registers with the agent-gateway on startup. The gateway is the single Service in front of the registry catalogue and the proxy through which other agents (and Backstage) reach this agent's endpoints.
Topology
sequenceDiagram
participant A as Agent pod
participant G as agent-gateway
Note over A: lifespan startup
A->>A: asyncio.sleep(2s)<br/>(let uvicorn bind)
A->>G: POST /agents/register-url<br/>{"endpoint": AGENT_SELF_URL}
G->>A: GET {AGENT_SELF_URL}/.well-known/agent.json
A-->>G: 200 OK + manifest
G-->>A: 201 Created (registration row)
Configuration
Two environment variables drive the flow. Both MUST be set in the agent's ConfigMap.
| Var | Example | Purpose |
|---|---|---|
REGISTRY_URL |
http://agent-gateway.agents.svc.cluster.local |
The gateway's base URL. |
AGENT_SELF_URL |
http://policy-transformer.agents.svc.cluster.local |
The URL the gateway should call back on. |
Notes:
- The registry validates
AGENT_SELF_URLagainst an SSRF allow-list. The host MUST end in.svc.cluster.localor.kyndemo.liveand usehttporhttps. Anything else is rejected with400 Bad Request. - Cross-namespace
AGENT_SELF_URLvalues are fine as long as the URL is resolvable in cluster DNS. - The trailing path on
AGENT_SELF_URLMUST be/(or empty); the gateway appends/.well-known/agent.jsonitself.
The registration call
The canonical implementation lives in template/app/agent.py. Key invariants:
- Initial sleep of 2 seconds before the first attempt — uvicorn needs time to bind the listen socket and the manifest route.
- 3 attempts total, 5 seconds between attempts, 10-second timeout per request.
- HTTP 200 or 201 are both treated as success.
- Logging:
INFOonce on success.WARNINGper failed attempt withHTTP <code>+ body excerpt.ERRORonce on final failure (after the third attempt).
- The task is created via
asyncio.create_taskinside the Starlettelifespan, so it does not block uvicorn from accepting traffic. A failed registration MUST NOT crash the agent.
What the gateway does next
After the agent's POST /agents/register-url succeeds, the gateway performs:
GET {endpoint}/.well-known/agent.json(hardcoded path).- Parses the manifest into an
AgentRegistrationrow keyed by the manifest'sname, normalised to lower-hyphen-case. - Registers the agent in the catalogue.
- Routes inbound traffic from
/proxy/agent-gateway/{agent_name}/{path}to{endpoint}/{path}.
Therefore the manifest at AGENT_SELF_URL MUST be reachable and MUST match the schema in docs/manifest.md.
Failure modes & resolution
| Symptom | Cause | Fix |
|---|---|---|
HTTP 400 — Failed to fetch agent card from .../.well-known/agent.json: HTTP 404 |
The agent has no manifest route. | Add Route("/.well-known/agent.json", agent_manifest, methods=["GET"]). |
HTTP 400 — ... HTTP 500 |
The manifest route raises (e.g. AgentSkill.model_dump AttributeError). |
Use the defensive _skill_dict() helper from the template. |
HTTP 400 — endpoint URL not allowed |
AGENT_SELF_URL host does not end in .svc.cluster.local or .kyndemo.live. |
Fix the ConfigMap. |
Three WARNINGs + final ERROR but agent serves traffic fine |
Gateway pod was restarting during agent startup. | Restart the agent pod once the gateway is Ready. Re-registration on next pod restart will recover. |
Local development
When running outside the cluster, leave REGISTRY_URL unset (or set it to "") — the agent will log REGISTRY_URL not set — skipping self-registration and continue.