feat(server): implement Agent Card endpoint with correct registry metadata #9600
@@ -0,0 +1,91 @@
|
||||
Feature: Agent Card endpoint for A2A protocol compliance
|
||||
As an A2A-compliant client
|
||||
I want to discover server capabilities and metadata
|
||||
So that I can auto-configure myself against the CleverAgents server
|
||||
|
||||
Scenario: Agent Card endpoint returns valid JSON
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the HTTP response content-type should be "application/json"
|
||||
|
||||
Scenario: Agent Card includes server identification
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the Agent Card should have name "CleverAgents"
|
||||
And the Agent Card should have version "1.0.0"
|
||||
And the Agent Card should have description containing "AI-powered"
|
||||
|
||||
Scenario: Agent Card includes provider information
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the Agent Card provider should have name "CleverThis Inc."
|
||||
And the Agent Card provider should have email "support@cleverthis.com"
|
||||
And the Agent Card provider should have url "https://cleverthis.com"
|
||||
|
||||
Scenario: Agent Card includes server capabilities
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the Agent Card should have at least 4 capabilities
|
||||
And the Agent Card should include capability "actor_management"
|
||||
And the Agent Card should include capability "skill_management"
|
||||
And the Agent Card should include capability "action_management"
|
||||
And the Agent Card should include capability "project_management"
|
||||
|
||||
Scenario: Agent Card includes A2A extension methods
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the Agent Card should have at least 4 extension methods
|
||||
And the Agent Card should include extension method "_cleveragents/actor/list"
|
||||
And the Agent Card should include extension method "_cleveragents/skill/list"
|
||||
And the Agent Card should include extension method "_cleveragents/action/list"
|
||||
And the Agent Card should include extension method "_cleveragents/project/list"
|
||||
|
||||
Scenario: Agent Card includes registry metadata
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the Agent Card registry should have at least 2 namespaces
|
||||
And the Agent Card registry should include namespace "default"
|
||||
And the Agent Card registry should include namespace "system"
|
||||
And the Agent Card registry should have at least 6 resource types
|
||||
And the Agent Card registry should include resource type "actor"
|
||||
And the Agent Card registry should include resource type "skill"
|
||||
And the Agent Card registry should include resource type "action"
|
||||
And the Agent Card registry should include resource type "project"
|
||||
|
||||
Scenario: Agent Card includes A2A protocol version
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the Agent Card should have a2a_version "2.0"
|
||||
|
||||
Scenario: Agent Card endpoint returns proper security headers
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP GET request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 200
|
||||
And the HTTP response should include a content-length header
|
||||
And the HTTP response should include header "x-content-type-options" with value "nosniff"
|
||||
And the HTTP response should include header "cache-control" with value "no-store"
|
||||
|
||||
Scenario: Agent Card endpoint is not accessible via POST
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP POST request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 405
|
||||
And the HTTP response should include an Allow header with value "GET"
|
||||
|
||||
Scenario: Agent Card endpoint is not accessible via PUT
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP PUT request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 405
|
||||
And the HTTP response should include an Allow header with value "GET"
|
||||
|
||||
Scenario: Agent Card endpoint is not accessible via DELETE
|
||||
Given the ASGI app module is loaded
|
||||
When I send an HTTP DELETE request to "/.well-known/agent.json" through the ASGI app
|
||||
Then the HTTP response status should be 405
|
||||
And the HTTP response should include an Allow header with value "GET"
|
||||
@@ -0,0 +1,190 @@
|
||||
"""Step definitions for Agent Card endpoint scenarios."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from behave import then
|
||||
from behave.runner import Context
|
||||
|
||||
SendMessage = dict[str, Any]
|
||||
|
||||
|
||||
def _parse_response(context: Context) -> None:
|
||||
"""Parse the HTTP response and extract body as JSON if applicable."""
|
||||
start = next(
|
||||
msg
|
||||
for msg in context.asgi_sent_messages
|
||||
if msg.get("type") == "http.response.start"
|
||||
)
|
||||
body_msg = next(
|
||||
msg
|
||||
for msg in context.asgi_sent_messages
|
||||
if msg.get("type") == "http.response.body"
|
||||
)
|
||||
|
||||
context.http_status = start.get("status")
|
||||
context.http_headers = start.get("headers", [])
|
||||
context.http_body_bytes = body_msg.get("body", b"")
|
||||
|
||||
# Try to parse as JSON
|
||||
try:
|
||||
context.http_body_json = json.loads(context.http_body_bytes.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
context.http_body_json = None
|
||||
|
||||
|
||||
@then('the HTTP response content-type should be "{content_type}"')
|
||||
def step_http_content_type(context: Context, content_type: str) -> None:
|
||||
headers: list[tuple[bytes, bytes]] = context.http_headers
|
||||
content_types = [
|
||||
v.decode("utf-8") for k, v in headers if k.lower() == b"content-type"
|
||||
]
|
||||
assert content_types, f"Expected content-type header, but none found in: {headers}"
|
||||
assert content_type in content_types, (
|
||||
f"Expected content-type {content_type!r}, got {content_types!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card should have name "{name}"')
|
||||
def step_agent_card_name(context: Context, name: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
assert context.http_body_json.get("name") == name, (
|
||||
f"Expected Agent Card name {name!r}, got {context.http_body_json.get('name')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card should have version "{version}"')
|
||||
def step_agent_card_version(context: Context, version: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
assert context.http_body_json.get("version") == version, (
|
||||
f"Expected Agent Card version {version!r}, got {context.http_body_json.get('version')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card should have description containing "{text}"')
|
||||
def step_agent_card_description(context: Context, text: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
description = context.http_body_json.get("description", "")
|
||||
assert text in description, (
|
||||
f"Expected description to contain {text!r}, got {description!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card provider should have name "{name}"')
|
||||
def step_agent_card_provider_name(context: Context, name: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
provider = context.http_body_json.get("provider", {})
|
||||
assert provider.get("name") == name, (
|
||||
f"Expected provider name {name!r}, got {provider.get('name')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card provider should have email "{email}"')
|
||||
def step_agent_card_provider_email(context: Context, email: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
provider = context.http_body_json.get("provider", {})
|
||||
assert provider.get("email") == email, (
|
||||
f"Expected provider email {email!r}, got {provider.get('email')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card provider should have url "{url}"')
|
||||
def step_agent_card_provider_url(context: Context, url: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
provider = context.http_body_json.get("provider", {})
|
||||
assert provider.get("url") == url, (
|
||||
f"Expected provider url {url!r}, got {provider.get('url')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the Agent Card should have at least {count:d} capabilities")
|
||||
def step_agent_card_capabilities_count(context: Context, count: int) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
capabilities = context.http_body_json.get("capabilities", [])
|
||||
assert len(capabilities) >= count, (
|
||||
f"Expected at least {count} capabilities, got {len(capabilities)}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card should include capability "{capability}"')
|
||||
def step_agent_card_includes_capability(context: Context, capability: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
capabilities = context.http_body_json.get("capabilities", [])
|
||||
capability_names = [c.get("name") for c in capabilities]
|
||||
assert capability in capability_names, (
|
||||
f"Expected capability {capability!r}, got {capability_names!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the Agent Card should have at least {count:d} extension methods")
|
||||
def step_agent_card_extension_methods_count(context: Context, count: int) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
methods = context.http_body_json.get("extension_methods", [])
|
||||
assert len(methods) >= count, (
|
||||
f"Expected at least {count} extension methods, got {len(methods)}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card should include extension method "{method}"')
|
||||
def step_agent_card_includes_extension_method(context: Context, method: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
methods = context.http_body_json.get("extension_methods", [])
|
||||
method_names = [m.get("method") for m in methods]
|
||||
assert method in method_names, (
|
||||
f"Expected extension method {method!r}, got {method_names!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the Agent Card registry should have at least {count:d} namespaces")
|
||||
def step_agent_card_registry_namespaces_count(context: Context, count: int) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
registry = context.http_body_json.get("registry", {})
|
||||
namespaces = registry.get("namespaces", [])
|
||||
assert len(namespaces) >= count, (
|
||||
f"Expected at least {count} namespaces, got {len(namespaces)}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card registry should include namespace "{namespace}"')
|
||||
def step_agent_card_registry_includes_namespace(context: Context, namespace: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
registry = context.http_body_json.get("registry", {})
|
||||
namespaces = registry.get("namespaces", [])
|
||||
assert namespace in namespaces, (
|
||||
f"Expected namespace {namespace!r}, got {namespaces!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the Agent Card registry should have at least {count:d} resource types")
|
||||
def step_agent_card_registry_resource_types_count(context: Context, count: int) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
registry = context.http_body_json.get("registry", {})
|
||||
resource_types = registry.get("resource_types", [])
|
||||
assert len(resource_types) >= count, (
|
||||
f"Expected at least {count} resource types, got {len(resource_types)}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card registry should include resource type "{resource_type}"')
|
||||
def step_agent_card_registry_includes_resource_type(
|
||||
context: Context, resource_type: str
|
||||
) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
registry = context.http_body_json.get("registry", {})
|
||||
resource_types = registry.get("resource_types", [])
|
||||
assert resource_type in resource_types, (
|
||||
f"Expected resource type {resource_type!r}, got {resource_types!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the Agent Card should have a2a_version "{version}"')
|
||||
def step_agent_card_a2a_version(context: Context, version: str) -> None:
|
||||
assert context.http_body_json is not None, "Response body is not valid JSON"
|
||||
assert context.http_body_json.get("a2a_version") == version, (
|
||||
f"Expected a2a_version {version!r}, got {context.http_body_json.get('a2a_version')!r}"
|
||||
)
|
||||
|
||||
|
||||
__all__ = []
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Any
|
||||
@@ -33,6 +34,7 @@ def step_send_http_request(context: Context, path: str) -> None:
|
||||
scope = {"type": "http", "method": "GET", "path": path}
|
||||
asyncio.run(context.asgi_app(scope, receive, send))
|
||||
context.asgi_sent_messages = sent_messages
|
||||
_parse_response(context)
|
||||
|
||||
|
||||
@when('I send an HTTP POST request to "{path}" through the ASGI app')
|
||||
@@ -48,6 +50,31 @@ def step_send_http_post_request(context: Context, path: str) -> None:
|
||||
scope = {"type": "http", "method": "POST", "path": path}
|
||||
asyncio.run(context.asgi_app(scope, receive, send))
|
||||
context.asgi_sent_messages = sent_messages
|
||||
_parse_response(context)
|
||||
|
||||
|
||||
def _parse_response(context: Context) -> None:
|
||||
"""Parse the HTTP response and extract body as JSON if applicable."""
|
||||
start = next(
|
||||
msg
|
||||
for msg in context.asgi_sent_messages
|
||||
if msg.get("type") == "http.response.start"
|
||||
)
|
||||
body_msg = next(
|
||||
msg
|
||||
for msg in context.asgi_sent_messages
|
||||
if msg.get("type") == "http.response.body"
|
||||
)
|
||||
|
||||
context.http_status = start.get("status")
|
||||
context.http_headers = start.get("headers", [])
|
||||
context.http_body_bytes = body_msg.get("body", b"")
|
||||
|
||||
# Try to parse as JSON
|
||||
try:
|
||||
context.http_body_json = json.loads(context.http_body_bytes.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
context.http_body_json = None
|
||||
|
||||
|
||||
@then("the HTTP response status should be {status:d}")
|
||||
@@ -281,3 +308,35 @@ class _capture_log_records:
|
||||
def __exit__(self, *_args: object) -> None:
|
||||
if self._handler is not None:
|
||||
self._logger.removeHandler(self._handler)
|
||||
|
||||
|
||||
@when('I send an HTTP PUT request to "{path}" through the ASGI app')
|
||||
def step_send_http_put_request(context: Context, path: str) -> None:
|
||||
sent_messages: list[SendMessage] = []
|
||||
|
||||
async def receive() -> dict[str, Any]:
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
async def send(message: SendMessage) -> None:
|
||||
sent_messages.append(message)
|
||||
|
||||
scope = {"type": "http", "method": "PUT", "path": path}
|
||||
asyncio.run(context.asgi_app(scope, receive, send))
|
||||
context.asgi_sent_messages = sent_messages
|
||||
_parse_response(context)
|
||||
|
||||
|
||||
@when('I send an HTTP DELETE request to "{path}" through the ASGI app')
|
||||
def step_send_http_delete_request(context: Context, path: str) -> None:
|
||||
sent_messages: list[SendMessage] = []
|
||||
|
||||
async def receive() -> dict[str, Any]:
|
||||
return {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
async def send(message: SendMessage) -> None:
|
||||
sent_messages.append(message)
|
||||
|
||||
scope = {"type": "http", "method": "DELETE", "path": path}
|
||||
asyncio.run(context.asgi_app(scope, receive, send))
|
||||
context.asgi_sent_messages = sent_messages
|
||||
_parse_response(context)
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Agent Card model for A2A protocol compliance.
|
||||
|
||||
The Agent Card is a standard A2A protocol document that describes the
|
||||
capabilities and metadata of a CleverAgents server. It is served at the
|
||||
well-known endpoint `/.well-known/agent.json` and allows A2A-compliant
|
||||
clients to auto-discover and configure themselves against the server.
|
||||
|
||||
Reference: A2A Protocol Specification - Agent Card
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from cleveragents import __version__
|
||||
|
||||
|
||||
class AgentCapability(BaseModel):
|
||||
"""Describes a single capability of the agent."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
name: str = Field(..., description="Name of the capability")
|
||||
description: str = Field(
|
||||
default="", description="Human-readable description of the capability"
|
||||
)
|
||||
version: str = Field(default="1.0.0", description="Version of the capability")
|
||||
|
||||
|
||||
class ExtensionMethod(BaseModel):
|
||||
"""Describes a supported A2A extension method."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
method: str = Field(
|
||||
..., description="Full method name (e.g., _cleveragents/actor/list)"
|
||||
)
|
||||
description: str = Field(
|
||||
default="", description="Human-readable description of the method"
|
||||
)
|
||||
params: dict[str, Any] = Field(
|
||||
default_factory=dict, description="Expected parameters for the method"
|
||||
)
|
||||
|
||||
|
||||
class RegistryMetadata(BaseModel):
|
||||
"""Describes registry metadata available on the server."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
namespaces: list[str] = Field(
|
||||
default_factory=list, description="Available namespaces in the registry"
|
||||
)
|
||||
resource_types: list[str] = Field(
|
||||
default_factory=list, description="Available resource types in the registry"
|
||||
)
|
||||
|
||||
|
||||
class ProviderInfo(BaseModel):
|
||||
"""Information about the provider/organization running the server."""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
name: str = Field(
|
||||
default="CleverThis Inc.", description="Provider organization name"
|
||||
)
|
||||
email: str = Field(
|
||||
default="support@cleverthis.com", description="Provider contact email"
|
||||
)
|
||||
url: str = Field(
|
||||
default="https://cleverthis.com", description="Provider website URL"
|
||||
)
|
||||
|
||||
|
||||
class AgentCard(BaseModel):
|
||||
"""Complete Agent Card document for A2A protocol compliance.
|
||||
|
||||
This model represents the complete metadata about a CleverAgents server
|
||||
that is served at `/.well-known/agent.json`. It includes server
|
||||
capabilities, supported A2A methods, registry metadata, and provider info.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(strict=False)
|
||||
|
||||
# Server identification
|
||||
name: str = Field(default="CleverAgents", description="Name of the agent/server")
|
||||
version: str = Field(
|
||||
default=__version__, description="Version of the CleverAgents server"
|
||||
)
|
||||
description: str = Field(
|
||||
default="AI-powered development assistant with A2A protocol support",
|
||||
description="Human-readable description of the server",
|
||||
)
|
||||
|
||||
# Provider information
|
||||
provider: ProviderInfo = Field(
|
||||
default_factory=ProviderInfo, description="Information about the provider"
|
||||
)
|
||||
|
||||
# Capabilities
|
||||
capabilities: list[AgentCapability] = Field(
|
||||
default_factory=list, description="List of server capabilities"
|
||||
)
|
||||
|
||||
# A2A extension methods
|
||||
extension_methods: list[ExtensionMethod] = Field(
|
||||
default_factory=list, description="Supported A2A extension methods"
|
||||
)
|
||||
|
||||
# Registry metadata
|
||||
registry: RegistryMetadata = Field(
|
||||
default_factory=RegistryMetadata, description="Registry metadata"
|
||||
)
|
||||
|
||||
# Protocol version
|
||||
a2a_version: str = Field(
|
||||
default="2.0", description="A2A protocol version supported"
|
||||
)
|
||||
|
||||
|
||||
def create_default_agent_card() -> AgentCard:
|
||||
"""Create a default Agent Card with standard CleverAgents capabilities.
|
||||
|
||||
Returns:
|
||||
AgentCard: A fully populated Agent Card with default values.
|
||||
"""
|
||||
return AgentCard(
|
||||
name="CleverAgents",
|
||||
version=__version__,
|
||||
description="AI-powered development assistant with A2A protocol support",
|
||||
provider=ProviderInfo(
|
||||
name="CleverThis Inc.",
|
||||
email="support@cleverthis.com",
|
||||
url="https://cleverthis.com",
|
||||
),
|
||||
capabilities=[
|
||||
AgentCapability(
|
||||
name="actor_management",
|
||||
description="Manage and execute actors",
|
||||
version="1.0.0",
|
||||
),
|
||||
AgentCapability(
|
||||
name="skill_management",
|
||||
description="Manage and execute skills",
|
||||
version="1.0.0",
|
||||
),
|
||||
AgentCapability(
|
||||
name="action_management",
|
||||
description="Manage and execute actions",
|
||||
version="1.0.0",
|
||||
),
|
||||
AgentCapability(
|
||||
name="project_management",
|
||||
description="Manage projects and workspaces",
|
||||
version="1.0.0",
|
||||
),
|
||||
],
|
||||
extension_methods=[
|
||||
ExtensionMethod(
|
||||
method="_cleveragents/actor/list",
|
||||
description="List all available actors",
|
||||
params={},
|
||||
),
|
||||
ExtensionMethod(
|
||||
method="_cleveragents/skill/list",
|
||||
description="List all available skills",
|
||||
params={},
|
||||
),
|
||||
ExtensionMethod(
|
||||
method="_cleveragents/action/list",
|
||||
description="List all available actions",
|
||||
params={},
|
||||
),
|
||||
ExtensionMethod(
|
||||
method="_cleveragents/project/list",
|
||||
description="List all available projects",
|
||||
params={},
|
||||
),
|
||||
],
|
||||
registry=RegistryMetadata(
|
||||
namespaces=["default", "system"],
|
||||
resource_types=[
|
||||
"actor",
|
||||
"skill",
|
||||
"action",
|
||||
"project",
|
||||
"plan",
|
||||
"resource",
|
||||
],
|
||||
),
|
||||
a2a_version="2.0",
|
||||
)
|
||||
@@ -11,9 +11,12 @@ for container and Kubernetes deployments.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from cleveragents.a2a.agent_card import create_default_agent_card
|
||||
|
||||
_logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
Headers = list[tuple[bytes, bytes]]
|
||||
@@ -21,7 +24,15 @@ SendCallable = Callable[[dict[str, object]], Awaitable[None]]
|
||||
|
||||
# Paths recognised by this ASGI app. Used to distinguish 404 (unknown
|
||||
# path) from 405 (known path, wrong method) per RFC 9110 S15.5.6.
|
||||
_KNOWN_PATHS: frozenset[str] = frozenset({"/", "/live", "/ready", "/health"})
|
||||
_KNOWN_PATHS: frozenset[str] = frozenset(
|
||||
{
|
||||
"/",
|
||||
"/live",
|
||||
"/ready",
|
||||
"/health",
|
||||
"/.well-known/agent.json",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _send_response(
|
||||
@@ -68,6 +79,7 @@ async def app(
|
||||
- ``GET /ready`` -> ``200`` with ``{"status":"ready"}``
|
||||
- ``GET /health`` -> ``200`` with ``{"status":"ok"}`` (compat alias)
|
||||
- ``GET /`` -> ``200`` with ``{"service":"cleveragents"}``
|
||||
- ``GET /.well-known/agent.json`` -> ``200`` with Agent Card JSON
|
||||
- known path, wrong method -> ``405``
|
||||
- unknown path -> ``404``
|
||||
"""
|
||||
@@ -121,6 +133,12 @@ async def app(
|
||||
await _send_response(send, status=200, body=b'{"service":"cleveragents"}')
|
||||
return
|
||||
|
||||
if method == "GET" and path == "/.well-known/agent.json":
|
||||
agent_card = create_default_agent_card()
|
||||
body = json.dumps(agent_card.model_dump(), indent=2).encode("utf-8")
|
||||
await _send_response(send, status=200, body=body)
|
||||
return
|
||||
|
||||
# Known path with wrong method -> 405 Method Not Allowed (RFC 9110 S15.5.6)
|
||||
if path in _KNOWN_PATHS:
|
||||
await _send_response(
|
||||
|
||||
Reference in New Issue
Block a user