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)
15 KiB
Platform Agent Harness Specification
Version: 1.0.0
Status: Draft → ratification pending
Applies to: All Python agents deployed under cjot-backstage-az/agents/ and any new agent on the platform.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119.
1. Scope
This spec defines the harness — the cross-cutting plumbing every agent shares — so that domain logic is the only thing an agent author has to write. It does not prescribe how an agent's business logic, prompts, or workflow internals are structured.
In scope:
- HTTP framework, entry point, and required routes
- Self-registration with the agent-gateway / agent-registry
- The
/.well-known/agent.jsonmanifest contract - OpenTelemetry wiring (logs, metrics, traces, LangChain instrumentation)
- Container image shape and build provenance
- Kubernetes deployment shape (namespace, RBAC, secrets, probes, OTel injection)
- Deployment scripts and the
.image-versioncadence
Out of scope:
- LangGraph workflow shape (recommended but not mandated — see §11.2)
- A2A SDK integration (recommended for multi-agent coordination — see §11.3)
- Domain-specific concerns (templates, git ops, multi-cloud auth, etc.)
2. Vocabulary
| Term | Meaning |
|---|---|
| Agent | A Python HTTP service that exposes domain skills, registered with the agent-gateway. |
| agent-registry / agent-gateway | The single Service (agent-gateway.agents.svc.cluster.local) that holds the catalogue of running agents and proxies external traffic to them. |
| Manifest | The JSON document served at /.well-known/agent.json describing the agent's identity, version, skills, and capabilities. |
| Harness | The non-domain plumbing every agent shares (routes, registration, OTel, k8s shape). |
| Operator | The OpenTelemetry Operator that injects the Python auto-instrumentation init container. |
3. Runtime requirements (MUST)
- The agent MUST be a Python ≥ 3.12 application.
- The agent MUST expose its HTTP server via Starlette. FastAPI is permitted only for legacy services (e.g.
agent-registry,support-intake-agent); new agents MUST use Starlette. - The ASGI application MUST be importable as
app.agent:app. - The container MUST run
uvicorn app.agent:app --host 0.0.0.0 --port <PORT>as PID 1. - The agent MUST listen on TCP port
8000or8080inside the container. Port8000is preferred for new agents.
4. Required HTTP routes (MUST)
Every agent MUST register the following routes:
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Liveness/readiness probe. Returns 200 OK with a JSON body when the process is up. |
GET |
/.well-known/agent.json |
A2A discovery manifest — see §6 and docs/manifest.md. |
Additional routes (the agent's domain endpoints) MAY be registered freely.
/health MUST return JSON of at least:
{
"status": "healthy",
"service": "<human-readable agent name>"
}
A version field is RECOMMENDED.
5. Self-registration with the gateway (MUST)
Every agent MUST self-register with the agent-gateway on startup. The flow is described in detail in docs/registration.md. In short:
- The agent MUST read these env vars:
REGISTRY_URL— the gateway's base URL (default:http://agent-gateway.agents.svc.cluster.local).AGENT_SELF_URL— the URL the gateway should call back on (default:http://<agent-name>.agents.svc.cluster.local).
- On startup (inside the Starlette
lifespancontext manager), the agent MUST spawn a background task that POSTs{"endpoint": AGENT_SELF_URL}to{REGISTRY_URL}/agents/register-url. - The registration call MUST retry at least 3 times with a 5-second backoff and a 2-second initial delay (to let uvicorn finish binding).
- Registration failure MUST NOT prevent the agent from serving traffic — it is logged at
ERRORand retried only via pod restart. - If
REGISTRY_URLis unset or empty, the agent MUST skip registration and log atINFO.
The gateway will then GET {AGENT_SELF_URL}/.well-known/agent.json to read the manifest. AGENT_SELF_URL MUST therefore be reachable from the gateway pod (i.e. resolve via cluster DNS) and serve a valid manifest.
6. Agent manifest (MUST)
GET /.well-known/agent.json MUST return a JSON document with at minimum:
{
"name": "Policy Transformer",
"version": "1.0.0",
"description": "...",
"url": "/",
"skills": [
{
"id": "policy_generation",
"name": "Policy Generation",
"description": "...",
"tags": ["policy", "generate"],
"examples": ["Generate a Kyverno policy that ..."]
}
],
"capabilities": {
"streaming": false,
"conversational": false,
"direct_api": true
}
}
skills MUST be a JSON array. If the agent uses the a2a-sdk AgentSkill type, it MUST serialise each skill defensively because AgentSkill is exposed as a protobuf Message in current a2a-sdk releases (not pydantic). Use a helper that handles model_dump, protobuf MessageToDict, and attribute fallback — see the canonical implementation in template/app/agent.py (_skill_dict).
Full schema and examples: docs/manifest.md.
7. OpenTelemetry wiring (MUST)
The full design is in docs/observability.md. Mandatory bullets:
7.1 Operator-driven SDK injection
The pod template MUST carry these annotations:
annotations:
instrumentation.opentelemetry.io/inject-python: "monitoring/otel-instrumentation"
instrumentation.opentelemetry.io/container-names: "<container-name>"
The container-names value MUST match the agent's container name.
7.2 Standard OTel env vars
The pod MUST set all six env vars below, in the agent container:
env:
- name: OTEL_SERVICE_NAME
value: "<agent-name>"
- name: OTEL_RESOURCE_ATTRIBUTES
value: "service.namespace=agents,service.version=<X.Y.Z>,deployment.environment=<env>,agent.type=<role>"
- name: OTEL_LOGS_EXPORTER
value: "otlp"
- name: OTEL_METRICS_EXPORTER
value: "otlp"
- name: OTEL_METRIC_EXPORT_INTERVAL
value: "15000"
- name: OTEL_SEMCONV_STABILITY_OPT_IN
value: "http"
Notes:
service.namespaceMUST be the Kubernetes namespace the agent runs in (typicallyagents).service.versionMUST match the contents of.image-version.agent.typeMUST be a stable, lower-hyphen identifier (e.g.policy-transformer,golden-path,registry). It is the primary metric/log label used in dashboards.OTEL_SEMCONV_STABILITY_OPT_IN=httpis required so the SDK emits stable HTTP semantic conventions (http.server.request.duration,http.response.status_code).
7.3 Logging bridge
The agent MUST ship app/logging_setup.py with a configure_otlp_log_handler(level) function that attaches a LoggingHandler to the root logger using the operator-provided LoggerProvider. The implementation MUST be a no-op when:
- The OTel SDK packages are not importable, or
get_logger_provider()returnsNoneor aNoOpLoggerProvider, or- A
LoggingHandleris already attached.
The canonical implementation is committed in template/app/logging_setup.py; copy it verbatim.
The agent MUST call configure_otlp_log_handler() once at module import time in app/agent.py.
7.4 Custom metrics
The agent MUST ship app/metrics.py that creates a meter via metrics.get_meter("<agent-name>", "<X.Y.Z>") and declares all custom instruments at module scope.
Naming MUST follow agent.<agent_type>.<verb_or_noun>[.duration|.errors], with:
unit="1"for counters,unit="s"for histograms timing seconds.
The agent MUST NOT call MeterProvider setters at runtime; the operator owns the global provider.
7.5 LLM instrumentation
If the agent calls a LangChain-style LLM, the agent MUST call LangChainInstrumentor().instrument() before the LLM client is constructed. This emits OpenInference spans with token counts and prompts/responses.
7.6 Tracing
Tracing of HTTP server, outbound HTTP, and FastAPI/Starlette routes is auto-instrumented by the operator. The agent SHOULD NOT install per-instrumentor wheels manually unless extending the default set.
8. Container image (MUST)
See template/Dockerfile for the canonical shape. Required properties:
- Base image MUST be
python:3.12-slim(or3.13-slimonce promoted by platform). - Image MUST install
requirements.txtat build time; runtimepip installis forbidden. - Image MUST NOT run as
root. Create and switch to a non-root user (UID 1001). - Image MUST expose only the chosen container port (8000 or 8080).
- Image SHOULD declare a
HEALTHCHECKcallingGET /health. - Image MUST be tagged with both
:latestand:<version>from.image-versionand pushed tobstagecjotdevacr.azurecr.io/<agent-name>.
9. Kubernetes manifests (MUST)
See docs/kubernetes.md for the full template. Required properties:
- The agent MUST ship
k8s/configmap.yamlandk8s/deployment.yaml(and theServicemay be inlined indeployment.yaml). - Namespace MUST be
agentsunless the agent has a strong domain reason for its own namespace (e.g.modernization-factory-v2runs inmodernization-factory); in that case,service.namespaceinOTEL_RESOURCE_ATTRIBUTESMUST match. - ServiceAccount MUST be
agents-sa(workload-identity bound). - The pod MUST mount the shared
agents-kv-spcSecretProviderClass at/mnt/secrets-storeand consume secrets viasecretKeyReffrom the syncedagents-kv-syncsecret. - The deployment MUST declare both
livenessProbeandreadinessProbeagainsthttpGet: { path: /health, port: http }. Recommended timing: livenessinitialDelaySeconds: 20, periodSeconds: 15; readinessinitialDelaySeconds: 10, periodSeconds: 10. Init delays MUST account for OTel SDK warm-up + LangGraph compile time. - The deployment MUST carry the OTel inject annotations from §7.1.
- The Service MUST expose port
80mapped to the container port (namedhttp),type: ClusterIP. - Resource requests/limits MUST be declared explicitly (no defaults).
10. Build & deployment (MUST)
See docs/deployment.md for the full flow. Required properties:
- The agent MUST ship a
.image-versionfile containing a single line of semver (X.Y.Z). - The agent MUST ship
scripts/deploy.shthat:- Auto-bumps the patch version of
.image-versionwhen no--tagis given, - Runs
az acr build --registry bstagecjotdevacr --image <agent>:<tag> --image <agent>:latest ., - Applies
k8s/configmap.yamlthenk8s/deployment.yaml, - Waits for
kubectl rollout status(recommended ≥ 180s timeout).
- Auto-bumps the patch version of
- The agent SHOULD ship
scripts/full-deploy.sh(preflight + build + apply + post-deploy/healthsmoke) andscripts/deploy-local.sh(docker/podman +.env.local). - Bumping
.image-versionMUST be paired with a code change in the same commit; image-version-only commits are reserved for redeploy reruns.
11. Recommended structure (SHOULD)
11.1 app/ layout
app/
├── __init__.py
├── agent.py # Starlette entry point, routes, lifespan
├── logging_setup.py # OTel log bridge (copy verbatim from template)
├── metrics.py # Meter + custom instruments
├── skills.py # AGENT_SKILLS list + AGENT_CONFIG dict
└── ... # workflows/, nodes/, tools/, states/, prompts/
11.2 LangGraph
Agents that drive a multi-step LLM workflow SHOULD use LangGraph and place graph definitions under app/workflows/, state TypedDicts under app/states.py (or app/states/), and node handlers under app/nodes/.
11.3 A2A SDK
Agents that participate in multi-agent coordination SHOULD integrate the a2a-sdk and mount its routes alongside the agent's domain routes. New agents MUST still serve /.well-known/agent.json themselves (do not rely on the SDK's manifest because it differs from the registry's expected shape).
11.4 Local dev
The agent SHOULD ship .env.local.example enumerating every env var read at runtime, with safe defaults committed and secrets blank.
12. Conformance checklist
Use this list to audit any agent (existing or new):
Code
- Python ≥ 3.12 (§3.1)
- Starlette app at
app.agent:app(§3.2–3.3) GET /healthreturns{"status": "healthy", ...}(§4)GET /.well-known/agent.jsonreturns the manifest defined in §6- Manifest serialisation handles pydantic + protobuf
AgentSkill(§6) _register_with_registry()POSTs to{REGISTRY_URL}/agents/register-urlwith 3×5s retry budget (§5)- Registration runs inside the Starlette
lifespanand does not block startup (§5) app/logging_setup.pymatches the canonical implementation and is invoked at import time (§7.3)app/metrics.pycreates the meter viametrics.get_meter(name, version)(§7.4)LangChainInstrumentor().instrument()is called before any LLM client construction (§7.5)
Image
Dockerfileusespython:3.12-slim, runs as non-root UID 1001, declaresHEALTHCHECK(§8)requirements.txtincludes the harness dependency floor (seetemplate/requirements.txt).image-versionfile present and matchesOTEL_RESOURCE_ATTRIBUTESservice.version(§10)
Kubernetes
- Namespace is
agents(or matchesservice.namespace) (§9.2) - ServiceAccount is
agents-sa(§9.3) - OTel inject annotations present and
container-namesmatches container name (§7.1) - All six standard OTel env vars present (§7.2)
agents-kv-spcSecretProviderClass mounted at/mnt/secrets-store(§9.4)- Liveness + readiness probes hit
/health(§9.5) - Service exposes port 80 → container port via named
http(§9.7) - Resource requests/limits declared (§9.8)
Deployment
scripts/deploy.shexists and auto-bumps.image-version(§10.2)scripts/full-deploy.shandscripts/deploy-local.shexist (§10.3 — SHOULD).env.local.examplepresent (§11.4 — SHOULD)
13. Versioning of this spec
The spec follows semver:
- MAJOR — breaking change to a
MUSTclause (e.g. registration endpoint changes shape). - MINOR — additive
MUSTorSHOULDclause. - PATCH — clarifications, examples, typo fixes.
See CHANGELOG.md. Agents SHOULD record the spec version they target in their README.md.