Files
agent-harness-spec/docs/manifest.md
Jonathan Boniface e6f10ba8c9 feat: initial harness spec v1.0.0 + reference template
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)
2026-06-09 15:54:18 +01:00

5.3 KiB

Agent manifest — /.well-known/agent.json

Normative requirements: SPEC.md §6

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).

Schema

{
  "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:

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:

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.

The route handler

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:

kubectl exec deployment/<agent> -c <container> -- \
  curl -fs http://localhost:<port>/.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.