Skip to content

Commit 12bd708

Browse files
authored
Merge pull request #7 from aliyun/feature/support_a2a
feat(a2a): add A2A support
2 parents 1c61fd3 + c18f516 commit 12bd708

117 files changed

Lines changed: 29201 additions & 124 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Makefile‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ install: ## Install dependencies and pre-commit hooks
1515
uv run pre-commit install
1616

1717
test: ## Run tests
18-
uv run pytest tests/ -v -n auto
18+
uv run --all-extras pytest tests/ -v -n auto
1919

2020
coverage: ## Run tests with coverage report (terminal + HTML)
21-
uv run pytest tests/ -n auto --cov --cov-report=term-missing --cov-report=html
21+
uv run --all-extras pytest tests/ -n auto --cov --cov-report=term-missing --cov-report=html
2222
@echo "HTML report: htmlcov/index.html"
2323

2424
lint: ## Run linters

‎pyproject.toml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ http = [
3232
"starlette>=0.39.0",
3333
"uvicorn[standard]>=0.30.0",
3434
]
35+
a2a = [
36+
"a2a-sdk[http-server,signing]>=1.0.2,<2",
37+
"cryptography>=42.0",
38+
"starlette>=0.39.0",
39+
"uvicorn[standard]>=0.30.0",
40+
]
41+
a2a-signing = [
42+
"a2a-sdk[signing]>=1.0.2,<2",
43+
]
44+
a2a-grpc = [
45+
"grpcio>=1.60.0",
46+
"grpcio-status>=1.60.0",
47+
]
48+
a2a-redis = [
49+
"redis>=5.0.0",
50+
]
3551

3652
[dependency-groups]
3753
dev = [
@@ -45,6 +61,7 @@ dev = [
4561
"pytest-cov>=5.0",
4662
"setuptools>=68.0",
4763
"wheel",
64+
"tomli>=2.0; python_version<\"3.11\"",
4865
]
4966

5067
[build-system]

‎src/iac_code/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
__version__ = "0.1.2"
2-
__release_date__ = ""
2+
__release_date__ = "2026-05-18"

‎src/iac_code/a2a/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""A2A protocol server support for iac-code."""

‎src/iac_code/a2a/agent_card.py‎

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from a2a.server.request_handlers.response_helpers import agent_card_to_dict
6+
from a2a.types import (
7+
AgentCapabilities,
8+
AgentCard,
9+
AgentCardSignature,
10+
AgentExtension,
11+
AgentInterface,
12+
AgentProvider,
13+
AgentSkill,
14+
APIKeySecurityScheme,
15+
HTTPAuthSecurityScheme,
16+
SecurityRequirement,
17+
)
18+
from google.protobuf.json_format import ParseDict
19+
20+
from iac_code import __version__
21+
from iac_code.a2a.parts import supported_input_mime_types
22+
from iac_code.a2a.signing import sign_agent_card_dict
23+
24+
IAC_CODE_ARTIFACT_METADATA_EXTENSION_URI = "urn:iac-code:a2a:artifact-metadata:v1"
25+
26+
27+
def _base_url(host: str, port: int) -> str:
28+
return f"http://{host}:{port}/"
29+
30+
31+
def agent_card_to_client_dict(card: AgentCard) -> dict[str, Any]:
32+
data = agent_card_to_dict(card)
33+
if not card.supported_interfaces:
34+
return data
35+
36+
primary_interface = card.supported_interfaces[0]
37+
data.setdefault("url", primary_interface.url)
38+
data.setdefault("preferredTransport", primary_interface.protocol_binding)
39+
data.setdefault("protocolVersion", primary_interface.protocol_version)
40+
41+
additional_interfaces = [
42+
{"url": interface.url, "transport": interface.protocol_binding} for interface in card.supported_interfaces[1:]
43+
]
44+
if additional_interfaces:
45+
data.setdefault("additionalInterfaces", additional_interfaces)
46+
47+
return data
48+
49+
50+
def _add_security_requirement(card: AgentCard, scheme_name: str) -> None:
51+
requirement = SecurityRequirement()
52+
requirement.schemes[scheme_name].list.append("")
53+
card.security_requirements.append(requirement)
54+
55+
56+
def build_agent_card(
57+
*,
58+
host: str,
59+
port: int,
60+
token_enabled: bool,
61+
basic_enabled: bool = False,
62+
api_key_enabled: bool = False,
63+
api_key_header: str = "X-API-Key",
64+
signing_secret: str | None = None,
65+
signing_key_id: str = "default",
66+
push_notifications: bool = False,
67+
supported_interfaces: list[dict[str, str]] | None = None,
68+
agent_extensions: Any = None,
69+
) -> AgentCard:
70+
url = _base_url(host, port)
71+
description = "AI-powered Infrastructure as Code assistant for Alibaba Cloud ROS and Terraform workflows."
72+
if not token_enabled and not basic_enabled and not api_key_enabled:
73+
description += " Unauthenticated A2A server mode is intended for trusted local environments."
74+
if push_notifications:
75+
description += (
76+
" Experimental terminal-state webhooks can be enabled locally, but the standard A2A push config API is not"
77+
" advertised."
78+
)
79+
80+
interfaces = (
81+
[
82+
AgentInterface(
83+
url=item["url"],
84+
protocol_binding=item["protocolBinding"],
85+
protocol_version=item.get("protocolVersion", "1.0"),
86+
)
87+
for item in supported_interfaces
88+
]
89+
if supported_interfaces
90+
else [
91+
AgentInterface(url=url, protocol_binding="JSONRPC", protocol_version="1.0"),
92+
]
93+
)
94+
input_modes = supported_input_mime_types()
95+
96+
card = AgentCard(
97+
name="iac-code",
98+
description=description,
99+
supported_interfaces=interfaces,
100+
provider=AgentProvider(organization="iac-code"),
101+
version=__version__,
102+
capabilities=AgentCapabilities(
103+
streaming=True,
104+
push_notifications=push_notifications,
105+
extended_agent_card=True,
106+
),
107+
default_input_modes=input_modes,
108+
default_output_modes=["text/plain"],
109+
skills=[
110+
AgentSkill(
111+
id="iac_generation",
112+
name="IaC Generation",
113+
description="Generate Alibaba Cloud ROS and Terraform templates from natural language.",
114+
tags=["iac", "ros", "terraform", "alibaba-cloud"],
115+
examples=["Create a VPC with two vSwitches in cn-hangzhou."],
116+
input_modes=input_modes,
117+
output_modes=["text/plain"],
118+
),
119+
AgentSkill(
120+
id="iac_review",
121+
name="IaC Review",
122+
description="Inspect IaC templates and suggest fixes.",
123+
tags=["iac", "review", "validation"],
124+
examples=["Review this ROS template for missing parameters."],
125+
input_modes=input_modes,
126+
output_modes=["text/plain"],
127+
),
128+
AgentSkill(
129+
id="aliyun_ros_operations",
130+
name="Alibaba Cloud ROS Operations",
131+
description="Assist with ROS stack workflows using iac-code tools.",
132+
tags=["aliyun", "ros", "stack"],
133+
examples=["Check why this ROS stack update failed."],
134+
input_modes=input_modes,
135+
output_modes=["text/plain"],
136+
),
137+
AgentSkill(
138+
id="terraform_ros_conversion",
139+
name="Terraform To ROS Conversion",
140+
description="Assist Terraform-to-ROS conversion using bundled iac-code skill resources.",
141+
tags=["terraform", "ros", "conversion"],
142+
examples=["Convert this Terraform VPC module to ROS YAML."],
143+
input_modes=input_modes,
144+
output_modes=["text/plain"],
145+
),
146+
],
147+
)
148+
card.capabilities.extensions.append(
149+
AgentExtension(
150+
uri=IAC_CODE_ARTIFACT_METADATA_EXTENSION_URI,
151+
description="Optional iac-code metadata namespace for tool status and stored local artifact metadata.",
152+
required=False,
153+
)
154+
)
155+
for item in _iter_agent_extensions(agent_extensions):
156+
card.capabilities.extensions.append(_agent_extension_from_dict(item))
157+
158+
if token_enabled:
159+
card.security_schemes["bearerAuth"].http_auth_security_scheme.CopyFrom(HTTPAuthSecurityScheme(scheme="bearer"))
160+
_add_security_requirement(card, "bearerAuth")
161+
162+
if basic_enabled:
163+
card.security_schemes["basicAuth"].http_auth_security_scheme.CopyFrom(HTTPAuthSecurityScheme(scheme="basic"))
164+
_add_security_requirement(card, "basicAuth")
165+
166+
if api_key_enabled:
167+
card.security_schemes["apiKeyAuth"].api_key_security_scheme.CopyFrom(
168+
APIKeySecurityScheme(location="header", name=api_key_header)
169+
)
170+
_add_security_requirement(card, "apiKeyAuth")
171+
172+
if signing_secret:
173+
signed_data = sign_agent_card_dict(agent_card_to_dict(card), secret=signing_secret, key_id=signing_key_id)
174+
signatures = signed_data.get("signatures")
175+
signature = signatures[0] if isinstance(signatures, list) and signatures else None
176+
if isinstance(signature, dict):
177+
header = signature.get("header")
178+
header_dict = dict(header) if isinstance(header, dict) else {}
179+
card_signature = AgentCardSignature(
180+
protected=str(signature.get("protected") or ""),
181+
signature=str(signature.get("signature") or ""),
182+
header=header_dict,
183+
)
184+
card.signatures.append(card_signature)
185+
186+
return card
187+
188+
189+
def build_extended_agent_card(card: AgentCard) -> AgentCard:
190+
extended = AgentCard()
191+
extended.CopyFrom(card)
192+
extended.skills.append(
193+
AgentSkill(
194+
id="iac_code_runtime_details",
195+
name="iac-code Runtime Details",
196+
description="Authenticated details for task management, push configuration, and local runtime behavior.",
197+
tags=["iac-code", "runtime", "a2a"],
198+
examples=["List my current A2A tasks."],
199+
input_modes=supported_input_mime_types(),
200+
output_modes=["text/plain"],
201+
)
202+
)
203+
return extended
204+
205+
206+
def _agent_extension_from_dict(item: dict[str, Any]) -> AgentExtension:
207+
extension = AgentExtension(
208+
uri=str(item["uri"]),
209+
description=str(item.get("description") or ""),
210+
required=bool(item.get("required", False)),
211+
)
212+
params = item.get("params")
213+
if isinstance(params, dict):
214+
ParseDict(params, extension.params)
215+
return extension
216+
217+
218+
def _iter_agent_extensions(value: Any) -> list[dict[str, Any]]:
219+
if not isinstance(value, list):
220+
return []
221+
return [item for item in value if isinstance(item, dict) and isinstance(item.get("uri"), str)]

0 commit comments

Comments
 (0)