deploy
All checks were successful
build / build (push) Successful in 4s

This commit is contained in:
a2a-platform
2026-05-27 15:53:43 +00:00
commit 16d7a5fc4f
9 changed files with 1252 additions and 0 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
__pycache__
*.pyc
.venv
.git
.pytest_cache
node_modules
.a2a
.env
.env.local

View File

@@ -0,0 +1,38 @@
name: build
on:
push:
branches: [main]
paths-ignore:
- 'deploy/**'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: build image
run: |
IMG=registry.a2acloud.io/agents/blog-openapi-agent
docker build --pull -t "$IMG:$GITHUB_SHA" -t "$IMG:latest" .
docker push "$IMG:$GITHUB_SHA"
docker push "$IMG:latest"
- name: bump deploy manifest
run: |
IMG=registry.a2acloud.io/agents/blog-openapi-agent
sed -i "s|image: $IMG:.*|image: $IMG:$GITHUB_SHA|" deploy/20-deployment.yaml
sed -i "s|value: $IMG:.*|value: $IMG:$GITHUB_SHA|" deploy/20-deployment.yaml
git config user.email "ci@a2a.local"
git config user.name "ci"
git add deploy/20-deployment.yaml
if git diff --staged --quiet; then
echo "no manifest changes"
else
git commit -m "ci: bump image to $GITHUB_SHA"
git push origin HEAD:main
fi

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM registry.a2acloud.io/a2a/a2a-pack-base:latest
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV A2A_ENTRYPOINT=agent:BlogOpenapiAgent
ENV PORT=8000
EXPOSE 8000
CMD a2a run --entrypoint "$A2A_ENTRYPOINT" --host 0.0.0.0 --port 8000

10
README.md Normal file
View File

@@ -0,0 +1,10 @@
# blog-openapi-agent
Generated A2APack agent for a2a cloud blog API.
- Source OpenAPI: https://blog.a2acloud.io/openapi.json
- Generated operations: 6
- Main skill: `auto`
The generated code is intentionally editable. Tune prompts, operation
grouping, auth names, and safety policy before publishing serious agents.

14
a2a.yaml Normal file
View File

@@ -0,0 +1,14 @@
name: blog-openapi-agent
version: 0.1.0
entrypoint: agent:BlogOpenapiAgent
description: Publishing and read API for the a2a cloud blog. Published content is
public; draft, preview, create, update, and delete operations require the blog API
key.
runtime:
resources:
cpu: 200m
memory: 512Mi
egress:
allow_hosts:
- blog.a2acloud.io
deny_internet_by_default: true

431
agent.py Normal file
View File

@@ -0,0 +1,431 @@
from __future__ import annotations
import base64
import json
from typing import Any
import httpx
from deepagents import create_deep_agent
from langchain_core.messages import BaseMessage
from langchain_core.tools import StructuredTool
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from a2a_pack import (
A2AAgent,
ConsumerSetup,
ConsumerSetupField,
ConsumerSetupMissing,
EgressPolicy,
LLMProvisioning,
Pricing,
Resources,
RunContext,
skill,
)
DEFAULT_BASE_URL = "https://blog.a2acloud.io"
OPERATIONS = json.loads("{\n \"create_blog_post\": {\n \"description\": \"Create a blog post\",\n \"destructive\": true,\n \"method\": \"POST\",\n \"operation_id\": \"create_blog_post\",\n \"parameters\": [],\n \"path\": \"/api/posts\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/BlogPostInput\"\n },\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"create_blog_post\",\n \"summary\": \"Create a blog post\",\n \"tags\": [\n \"Posts\"\n ]\n },\n \"delete_blog_post\": {\n \"description\": \"Delete a post by slug\",\n \"destructive\": true,\n \"method\": \"DELETE\",\n \"operation_id\": \"delete_blog_post\",\n \"parameters\": [],\n \"path\": \"/api/posts/{slug}\",\n \"request_body\": null,\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"delete_blog_post\",\n \"summary\": \"Delete a post by slug\",\n \"tags\": [\n \"Posts\"\n ]\n },\n \"get_blog_health\": {\n \"description\": \"Check blog service health\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"get_blog_health\",\n \"parameters\": [],\n \"path\": \"/api/healthz\",\n \"request_body\": null,\n \"security\": null,\n \"skill_name\": \"get_blog_health\",\n \"summary\": \"Check blog service health\",\n \"tags\": [\n \"Health\"\n ]\n },\n \"get_blog_post\": {\n \"description\": \"Returns a published post. Add `preview=1` to fetch drafts or future-dated posts; preview requests require either bearer auth or the `x-api-key` header.\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"get_blog_post\",\n \"parameters\": [\n {\n \"description\": \"Set to `1` to bypass the public published-post filter. Requires authentication.\",\n \"in\": \"query\",\n \"name\": \"preview\",\n \"required\": false,\n \"schema\": {\n \"enum\": [\n \"1\"\n ],\n \"type\": \"string\"\n }\n }\n ],\n \"path\": \"/api/posts/{slug}\",\n \"request_body\": null,\n \"security\": [\n {},\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"get_blog_post\",\n \"summary\": \"Get a post by slug\",\n \"tags\": [\n \"Posts\"\n ]\n },\n \"list_blog_posts\": {\n \"description\": \"Returns published posts by default. `status=all` and `status=draft` include draft content and require either bearer auth or the `x-api-key` header.\",\n \"destructive\": false,\n \"method\": \"GET\",\n \"operation_id\": \"list_blog_posts\",\n \"parameters\": [\n {\n \"description\": \"Use `published` or omit the parameter for public published posts. `all` and `draft` require authentication.\",\n \"in\": \"query\",\n \"name\": \"status\",\n \"required\": false,\n \"schema\": {\n \"default\": \"published\",\n \"enum\": [\n \"published\",\n \"draft\",\n \"all\"\n ],\n \"type\": \"string\"\n }\n }\n ],\n \"path\": \"/api/posts\",\n \"request_body\": null,\n \"security\": [\n {},\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"list_blog_posts\",\n \"summary\": \"List posts\",\n \"tags\": [\n \"Posts\"\n ]\n },\n \"upsert_blog_post\": {\n \"description\": \"Upsert a post by slug\",\n \"destructive\": true,\n \"method\": \"PUT\",\n \"operation_id\": \"upsert_blog_post\",\n \"parameters\": [],\n \"path\": \"/api/posts/{slug}\",\n \"request_body\": {\n \"$ref\": \"#/components/schemas/BlogPostInput\"\n },\n \"security\": [\n {\n \"bearerAuth\": []\n },\n {\n \"apiKeyAuth\": []\n }\n ],\n \"skill_name\": \"upsert_blog_post\",\n \"summary\": \"Upsert a post by slug\",\n \"tags\": [\n \"Posts\"\n ]\n }\n}")
ROOT_SECURITY = json.loads("[]")
SECURITY_SCHEMES = json.loads("{\n \"apiKeyAuth\": {\n \"description\": \"Use the configured `BLOG_API_KEY` value.\",\n \"in\": \"header\",\n \"name\": \"x-api-key\",\n \"type\": \"apiKey\"\n },\n \"bearerAuth\": {\n \"description\": \"Use `Authorization: Bearer $BLOG_API_KEY`.\",\n \"scheme\": \"bearer\",\n \"type\": \"http\"\n }\n}")
SECURITY_FIELDS = json.loads("{\n \"apiKeyAuth\": {\n \"field\": \"X_API_KEY\",\n \"kind\": \"apiKey\",\n \"location\": \"header\",\n \"name\": \"x-api-key\"\n },\n \"bearerAuth\": {\n \"field\": \"BEARERAUTH_TOKEN\",\n \"kind\": \"http\",\n \"scheme\": \"bearer\"\n }\n}")
class OperationInput(BaseModel):
parameters: dict[str, Any] = Field(
default_factory=dict,
description="Path, query, header, and cookie parameters keyed by OpenAPI parameter name.",
)
body: Any | None = Field(default=None, description="JSON request body, when the operation accepts one.")
class BlogOpenapiAgent(A2AAgent):
name = "blog-openapi-agent"
description = "Publishing and read API for the a2a cloud blog. Published content is public; draft, preview, create, update, and delete operations require the blog API key."
version = "0.1.0"
consumer_setup = ConsumerSetup.from_fields(
ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://blog.a2acloud.io).", required=False, input_type="url"),
ConsumerSetupField.secret("BEARERAUTH_TOKEN", label="bearerAuth bearer credential", description="Use `Authorization: Bearer $BLOG_API_KEY`.", required=False),
ConsumerSetupField.secret("X_API_KEY", label="apiKeyAuth API key", description="Use the configured `BLOG_API_KEY` value.", required=False),
)
llm_provisioning = LLMProvisioning.PLATFORM
pricing = Pricing(
price_per_call_usd=0.0,
caller_pays_llm=False,
notes="Uses a scoped platform LLM grant through ctx.llm.",
)
resources = Resources(cpu="200m", memory="512Mi")
egress = EgressPolicy(
allow_hosts=('blog.a2acloud.io',),
deny_internet_by_default=True,
)
tools_used = ("openapi", "deepagents")
capabilities = {
"openapi_auto_agent": {
"operation_count": len(OPERATIONS),
"default_base_url": DEFAULT_BASE_URL,
"security_schemes": list(SECURITY_SCHEMES),
}
}
@skill(
name="auto",
description="Use the OpenAPI service to complete a natural-language goal.",
tags=("openapi", "auto"),
timeout_seconds=180,
)
async def auto(self, ctx: RunContext, goal: str) -> dict[str, Any]:
creds = ctx.llm
if not creds.api_key:
return {
"error": "llm_credentials_missing",
"final": (
"LLM credentials were not available for this run. Hosted "
"generated OpenAPI agents should receive a platform LLM grant."
),
"messages": [],
}
model_kwargs: dict[str, Any] = {
"model": creds.model,
"base_url": creds.base_url,
"api_key": creds.api_key,
}
if creds.temperature_mode != "omit" and creds.temperature is not None:
model_kwargs["temperature"] = creds.temperature
if creds.extra_body:
model_kwargs["extra_body"] = dict(creds.extra_body)
model = ChatOpenAI(**model_kwargs)
graph = create_deep_agent(
model=model,
tools=self._operation_tools(ctx),
system_prompt=self._system_prompt(),
)
result = await graph.ainvoke({"messages": [{"role": "user", "content": goal}]})
messages = result.get("messages", []) if isinstance(result, dict) else []
final = _message_text(messages[-1]) if messages else result
return {
"final": final,
"messages": [_message_to_dict(message) for message in messages[-8:]],
}
@skill(
name="get_blog_health",
description="GET /api/healthz - Check blog service health",
tags=('Health',),
timeout_seconds=60,
)
async def get_blog_health(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"get_blog_health",
parameters=parameters or {},
body=body,
)
@skill(
name="list_blog_posts",
description="GET /api/posts - List posts",
tags=('Posts',),
timeout_seconds=60,
)
async def list_blog_posts(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"list_blog_posts",
parameters=parameters or {},
body=body,
)
@skill(
name="create_blog_post",
description="POST /api/posts - Create a blog post",
tags=('Posts',),
timeout_seconds=120,
)
async def create_blog_post(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"create_blog_post",
parameters=parameters or {},
body=body,
)
@skill(
name="get_blog_post",
description="GET /api/posts/{slug} - Get a post by slug",
tags=('Posts',),
timeout_seconds=60,
)
async def get_blog_post(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"get_blog_post",
parameters=parameters or {},
body=body,
)
@skill(
name="upsert_blog_post",
description="PUT /api/posts/{slug} - Upsert a post by slug",
tags=('Posts',),
timeout_seconds=120,
)
async def upsert_blog_post(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"upsert_blog_post",
parameters=parameters or {},
body=body,
)
@skill(
name="delete_blog_post",
description="DELETE /api/posts/{slug} - Delete a post by slug",
tags=('Posts',),
timeout_seconds=120,
)
async def delete_blog_post(
self,
ctx: RunContext,
parameters: dict[str, Any] | None = None,
body: Any | None = None,
) -> dict[str, Any]:
return await self._request(
ctx,
"delete_blog_post",
parameters=parameters or {},
body=body,
)
def _operation_tools(self, ctx: RunContext) -> list[StructuredTool]:
tools: list[StructuredTool] = []
for operation_id, operation in OPERATIONS.items():
async def call(
parameters: dict[str, Any] | None = None,
body: Any | None = None,
*,
_operation_id: str = operation_id,
) -> dict[str, Any]:
return await self._request(
ctx,
_operation_id,
parameters=parameters or {},
body=body,
)
tools.append(
StructuredTool.from_function(
coroutine=call,
name=operation["skill_name"],
description=self._tool_description(operation),
args_schema=OperationInput,
)
)
return tools
def _system_prompt(self) -> str:
lines = [
"You operate an API through generated OpenAPI tools.",
"Call tools to get real results. Do not invent API responses.",
"For write, update, or delete operations, explain the intended action before calling the tool.",
"If an operation reports missing consumer setup, tell the user which setup field is required.",
"",
"Available operations:",
]
for operation in OPERATIONS.values():
marker = "WRITE" if operation.get("destructive") else "READ"
lines.append(
f"- {operation['skill_name']}: {marker} {operation['method']} {operation['path']}{operation['summary']}"
)
return "\n".join(lines)
def _tool_description(self, operation: dict[str, Any]) -> str:
parts = [
f"{operation['method']} {operation['path']}",
str(operation.get("description") or operation.get("summary") or ""),
]
if operation.get("parameters"):
parts.append(
"Parameters: "
+ ", ".join(
f"{p['name']} in {p['in']}{' required' if p.get('required') else ''}"
for p in operation["parameters"]
)
)
if operation.get("request_body") is not None:
parts.append("Accepts a JSON request body.")
return "\n".join(part for part in parts if part)
async def _request(
self,
ctx: RunContext,
operation_id: str,
*,
parameters: dict[str, Any],
body: Any | None,
) -> dict[str, Any]:
operation = OPERATIONS[operation_id]
base_url = str(ctx.consumer_config("OPENAPI_BASE_URL", DEFAULT_BASE_URL) or DEFAULT_BASE_URL).rstrip("/")
url, query, headers = self._request_parts(ctx, operation, parameters)
request_kwargs: dict[str, Any] = {
"params": query,
"headers": headers,
}
if body is not None:
request_kwargs["json"] = body
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
response = await client.request(
operation["method"],
f"{base_url}{url}",
**request_kwargs,
)
content_type = response.headers.get("content-type", "")
try:
payload: Any = response.json() if "json" in content_type else response.text
except ValueError:
payload = response.text
if response.status_code >= 400:
return {
"ok": False,
"status_code": response.status_code,
"operation_id": operation_id,
"error": payload,
}
return {
"ok": True,
"status_code": response.status_code,
"operation_id": operation_id,
"result": payload,
}
def _request_parts(
self,
ctx: RunContext,
operation: dict[str, Any],
parameters: dict[str, Any],
) -> tuple[str, dict[str, Any], dict[str, str]]:
path = operation["path"]
query: dict[str, Any] = {}
headers: dict[str, str] = {}
cookies: dict[str, Any] = {}
for param in operation.get("parameters") or []:
name = param["name"]
if name not in parameters:
if param.get("required"):
raise ValueError(f"missing required parameter {name!r}")
continue
value = parameters[name]
location = param["in"]
if location == "path":
path = path.replace(" + name + ", str(value))
elif location == "query":
query[name] = value
elif location == "header":
headers[name] = str(value)
elif location == "cookie":
cookies[name] = value
if cookies:
headers["cookie"] = "; ".join(f"{key}={value}" for key, value in cookies.items())
auth_headers, auth_query = self._auth_for_operation(ctx, operation)
headers.update(auth_headers)
query.update(auth_query)
return path, query, headers
def _auth_for_operation(self, ctx: RunContext, operation: dict[str, Any]) -> tuple[dict[str, str], dict[str, Any]]:
requirements = operation.get("security")
if requirements is None:
requirements = ROOT_SECURITY
if requirements == []:
return {}, {}
missing: list[str] = []
for requirement in requirements or []:
if not isinstance(requirement, dict) or not requirement:
return {}, {}
headers: dict[str, str] = {}
query: dict[str, Any] = {}
ok = True
for scheme_name in requirement:
mapping = SECURITY_FIELDS.get(scheme_name) or {}
secret_name = mapping.get("field")
value = _consumer_secret_optional(ctx, secret_name) if secret_name else ""
if not value:
ok = False
if secret_name:
missing.append(secret_name)
continue
kind = mapping.get("kind")
if kind == "apiKey":
if mapping.get("location") == "query":
query[mapping.get("name") or scheme_name] = value
else:
headers[mapping.get("name") or scheme_name] = value
elif kind in {"http", "oauth2"}:
scheme = mapping.get("scheme") or "Bearer"
if scheme.lower() == "basic":
token = base64.b64encode(value.encode("utf-8")).decode("ascii")
headers["authorization"] = f"Basic {token}"
else:
headers["authorization"] = f"{scheme} {value}"
else:
ok = False
if ok:
return headers, query
if missing:
raise ConsumerSetupMissing(
"operation requires consumer setup secret(s): "
+ ", ".join(sorted(set(missing)))
)
return {}, {}
def _consumer_secret_optional(ctx: RunContext, name: str | None) -> str:
if not name:
return ""
try:
return ctx.consumer_secret(name)
except ConsumerSetupMissing:
return ""
def _message_text(message: Any) -> Any:
content = getattr(message, "content", message)
return content
def _message_to_dict(message: Any) -> dict[str, Any]:
if isinstance(message, BaseMessage):
return message.model_dump(mode="json")
if hasattr(message, "model_dump"):
return message.model_dump(mode="json")
if isinstance(message, dict):
return message
return {"content": str(message)}
agent = BlogOpenapiAgent()

102
deploy/20-deployment.yaml Normal file
View File

@@ -0,0 +1,102 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: blog-openapi-agent
namespace: agents
labels:
app: blog-openapi-agent
a2a/managed-by: control-plane
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: blog-openapi-agent
template:
metadata:
labels:
app: blog-openapi-agent
spec:
containers:
- name: agent
image: registry.a2acloud.io/agents/blog-openapi-agent:latest
imagePullPolicy: Always
env:
- name: A2A_AGENT_IMAGE
value: registry.a2acloud.io/agents/blog-openapi-agent:latest
- name: A2A_RENDER_IMAGE
value: registry.a2acloud.io/agents/blog-openapi-agent:latest
- name: A2A_CP_URL
value: "http://control-plane.control-plane.svc.cluster.local"
- name: A2A_LOGIN_URL
value: "https://app.a2acloud.io"
- name: A2A_SESSION_COOKIE_NAME
value: "a2a_session"
- name: A2A_GRANT_VERIFYING_KEY
valueFrom:
secretKeyRef: {name: platform-secrets, key: grant_verifying_key}
- name: A2A_SANDBOX_URL
value: http://sandbox.sandbox.svc.cluster.local:8000
- name: A2A_SANDBOX_TIMEOUT_S
value: "180"
envFrom:
- secretRef:
name: blog-openapi-agent-agent-secrets
optional: true
ports:
- containerPort: 8000
name: http
readinessProbe:
httpGet: {path: /healthz, port: 8000}
initialDelaySeconds: 2
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 6
livenessProbe:
httpGet: {path: /healthz, port: 8000}
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 6
resources:
requests: {cpu: "200m", memory: "512Mi"}
limits: {cpu: "200m", memory: "512Mi"}
---
apiVersion: v1
kind: Service
metadata:
name: blog-openapi-agent
namespace: agents
spec:
type: ClusterIP
selector:
app: blog-openapi-agent
ports:
- name: http
port: 80
targetPort: 8000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: blog-openapi-agent
namespace: agents
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
spec:
tls:
- hosts: [blog-openapi-agent.a2acloud.io]
secretName: blog-openapi-agent-a2acloud-io-tls
rules:
- host: blog-openapi-agent.a2acloud.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: blog-openapi-agent
port:
number: 80

626
openapi.json Normal file
View File

@@ -0,0 +1,626 @@
{
"components": {
"parameters": {
"Slug": {
"description": "Post slug. The API normalizes path slugs to lowercase kebab-case before lookup.",
"in": "path",
"name": "slug",
"required": true,
"schema": {
"example": "shipping-agents-with-receipts",
"maxLength": 120,
"minLength": 1,
"type": "string"
}
}
},
"responses": {
"AuthNotConfigured": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "`BLOG_API_KEY` is not configured on the server."
},
"BadRequest": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "The request body, slug, or query parameters are invalid."
},
"NotFound": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "The requested post was not found."
},
"Unauthorized": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "The blog API key is missing or invalid."
}
},
"schemas": {
"BlogPost": {
"properties": {
"author": {
"example": "a2a cloud",
"type": "string"
},
"content": {
"example": "# Shipping agents with receipts\n\nAgents need durable runtime boundaries.",
"type": "string"
},
"cover_image_url": {
"example": "https://blog.a2acloud.io/brand/og-image.png",
"format": "uri",
"maxLength": 500,
"type": [
"string",
"null"
]
},
"created_at": {
"format": "date-time",
"type": "string"
},
"excerpt": {
"example": "Why agent infrastructure needs identity, authority, and proof.",
"maxLength": 420,
"type": "string"
},
"id": {
"example": 42,
"format": "int64",
"type": "integer"
},
"published_at": {
"format": "date-time",
"type": [
"string",
"null"
]
},
"slug": {
"example": "shipping-agents-with-receipts",
"type": "string"
},
"status": {
"$ref": "#/components/schemas/BlogStatus"
},
"tags": {
"example": [
"platform",
"agents"
],
"items": {
"type": "string"
},
"maxItems": 12,
"type": "array"
},
"title": {
"example": "Shipping agents with receipts",
"maxLength": 180,
"type": "string"
},
"updated_at": {
"format": "date-time",
"type": "string"
}
},
"required": [
"id",
"slug",
"title",
"excerpt",
"content",
"author",
"status",
"tags",
"cover_image_url",
"created_at",
"updated_at",
"published_at"
],
"type": "object"
},
"BlogPostInput": {
"properties": {
"author": {
"default": "a2a cloud",
"maxLength": 120,
"type": "string"
},
"content": {
"example": "# Shipping agents with receipts\n\nAgents need durable runtime boundaries.",
"maxLength": 120000,
"minLength": 1,
"type": "string"
},
"cover_image_url": {
"format": "uri",
"maxLength": 500,
"type": "string"
},
"excerpt": {
"default": "",
"example": "Why agent infrastructure needs identity, authority, and proof.",
"maxLength": 420,
"type": "string"
},
"published_at": {
"description": "Defaults to the current time for published posts and null for drafts.",
"format": "date-time",
"type": "string"
},
"slug": {
"description": "Optional on create. The path slug is used for upserts. Slugs are normalized to lowercase kebab-case.",
"example": "shipping-agents-with-receipts",
"maxLength": 120,
"type": "string"
},
"status": {
"$ref": "#/components/schemas/BlogStatus",
"default": "published"
},
"tags": {
"default": [],
"items": {
"type": "string"
},
"maxItems": 12,
"type": "array"
},
"title": {
"example": "Shipping agents with receipts",
"maxLength": 180,
"minLength": 1,
"type": "string"
}
},
"required": [
"title",
"content"
],
"type": "object"
},
"BlogStatus": {
"enum": [
"draft",
"published"
],
"type": "string"
},
"DeleteResponse": {
"properties": {
"ok": {
"const": true,
"type": "boolean"
}
},
"required": [
"ok"
],
"type": "object"
},
"ErrorResponse": {
"properties": {
"error": {
"type": "string"
}
},
"required": [
"error"
],
"type": "object"
},
"HealthError": {
"properties": {
"error": {
"type": "string"
},
"ok": {
"const": false,
"type": "boolean"
}
},
"required": [
"ok",
"error"
],
"type": "object"
},
"HealthOk": {
"properties": {
"ok": {
"const": true,
"type": "boolean"
}
},
"required": [
"ok"
],
"type": "object"
},
"PostListResponse": {
"properties": {
"posts": {
"items": {
"$ref": "#/components/schemas/BlogPost"
},
"type": "array"
}
},
"required": [
"posts"
],
"type": "object"
},
"PostResponse": {
"properties": {
"post": {
"$ref": "#/components/schemas/BlogPost"
}
},
"required": [
"post"
],
"type": "object"
}
},
"securitySchemes": {
"apiKeyAuth": {
"description": "Use the configured `BLOG_API_KEY` value.",
"in": "header",
"name": "x-api-key",
"type": "apiKey"
},
"bearerAuth": {
"description": "Use `Authorization: Bearer $BLOG_API_KEY`.",
"scheme": "bearer",
"type": "http"
}
}
},
"info": {
"description": "Publishing and read API for the a2a cloud blog. Published content is public; draft, preview, create, update, and delete operations require the blog API key.",
"title": "a2a cloud blog API",
"version": "0.1.0"
},
"openapi": "3.1.0",
"paths": {
"/api/healthz": {
"get": {
"operationId": "getBlogHealth",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HealthOk"
}
}
},
"description": "The service can initialize its schema and query Postgres."
},
"503": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HealthError"
}
}
},
"description": "The service or database is unavailable."
}
},
"summary": "Check blog service health",
"tags": [
"Health"
]
}
},
"/api/posts": {
"get": {
"description": "Returns published posts by default. `status=all` and `status=draft` include draft content and require either bearer auth or the `x-api-key` header.",
"operationId": "listBlogPosts",
"parameters": [
{
"description": "Use `published` or omit the parameter for public published posts. `all` and `draft` require authentication.",
"in": "query",
"name": "status",
"required": false,
"schema": {
"default": "published",
"enum": [
"published",
"draft",
"all"
],
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PostListResponse"
}
}
},
"description": "Posts returned in newest-first order."
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"503": {
"$ref": "#/components/responses/AuthNotConfigured"
}
},
"security": [
{},
{
"bearerAuth": []
},
{
"apiKeyAuth": []
}
],
"summary": "List posts",
"tags": [
"Posts"
]
},
"post": {
"operationId": "createBlogPost",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BlogPostInput"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PostResponse"
}
}
},
"description": "The post was created."
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"409": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": "A post with this slug already exists."
},
"503": {
"$ref": "#/components/responses/AuthNotConfigured"
}
},
"security": [
{
"bearerAuth": []
},
{
"apiKeyAuth": []
}
],
"summary": "Create a blog post",
"tags": [
"Posts"
]
}
},
"/api/posts/{slug}": {
"delete": {
"operationId": "deleteBlogPost",
"parameters": [
{
"$ref": "#/components/parameters/Slug"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteResponse"
}
}
},
"description": "The post was deleted."
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"503": {
"$ref": "#/components/responses/AuthNotConfigured"
}
},
"security": [
{
"bearerAuth": []
},
{
"apiKeyAuth": []
}
],
"summary": "Delete a post by slug",
"tags": [
"Posts"
]
},
"get": {
"description": "Returns a published post. Add `preview=1` to fetch drafts or future-dated posts; preview requests require either bearer auth or the `x-api-key` header.",
"operationId": "getBlogPost",
"parameters": [
{
"$ref": "#/components/parameters/Slug"
},
{
"description": "Set to `1` to bypass the public published-post filter. Requires authentication.",
"in": "query",
"name": "preview",
"required": false,
"schema": {
"enum": [
"1"
],
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PostResponse"
}
}
},
"description": "The matching post."
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"503": {
"$ref": "#/components/responses/AuthNotConfigured"
}
},
"security": [
{},
{
"bearerAuth": []
},
{
"apiKeyAuth": []
}
],
"summary": "Get a post by slug",
"tags": [
"Posts"
]
},
"put": {
"operationId": "upsertBlogPost",
"parameters": [
{
"$ref": "#/components/parameters/Slug"
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BlogPostInput"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PostResponse"
}
}
},
"description": "The post was created or updated."
},
"400": {
"$ref": "#/components/responses/BadRequest"
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"503": {
"$ref": "#/components/responses/AuthNotConfigured"
}
},
"security": [
{
"bearerAuth": []
},
{
"apiKeyAuth": []
}
],
"summary": "Upsert a post by slug",
"tags": [
"Posts"
]
}
}
},
"servers": [
{
"description": "Production",
"url": "https://blog.a2acloud.io"
},
{
"description": "Local development",
"url": "http://localhost:3000"
}
],
"tags": [
{
"description": "Blog post listing, publishing, and lookup.",
"name": "Posts"
},
{
"description": "Service health checks.",
"name": "Health"
}
]
}

7
requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
# a2a-pack is installed by the platform base image.
deepagents>=0.5.0
langchain>=0.3
langchain-openai>=0.2
langchain-core>=0.3
langgraph>=0.6
httpx>=0.27