ship grants, a2a_client, discovery, sandbox SDK + tests

This commit is contained in:
robert
2026-05-09 12:43:07 -03:00
parent b6f6cd1643
commit 2dcb8a09cd
15 changed files with 1853 additions and 75 deletions

View File

@@ -24,10 +24,12 @@ from pydantic import BaseModel
from ..agent import A2AAgent, SkillInputError, SkillNotFound
from ..auth import APIKeyAuth, NoAuth
from ..context import LocalRunContext, MissingScopes
from ..grants import Grant, GrantInvalid, verify_grant
class _InvokeIn(BaseModel):
arguments: dict[str, Any] = {}
grant: str | None = None
def build_app(agent: A2AAgent) -> FastAPI:
@@ -76,8 +78,23 @@ def build_app(agent: A2AAgent) -> FastAPI:
authorization: str | None = Header(default=None),
) -> dict[str, Any]:
token = _check_key(authorization)
# If the caller handed us a grant, verify it and build a
# workspace bounded by its claims. Production runtimes plug in a
# real MinIO-backed WorkspaceClient here; for now we materialize
# an empty in-memory view so policy enforcement is exercised.
granted_workspace = None
grant_obj: Grant | None = None
if body.grant is not None:
try:
grant_obj = verify_grant(body.grant)
except GrantInvalid as exc:
raise HTTPException(403, f"invalid grant: {exc}") from exc
granted_workspace = _grant_to_workspace(grant_obj, agent)
ctx: LocalRunContext[Any] = LocalRunContext(
auth=_build_auth(token), task_id=f"http-{skill_name}"
auth=_build_auth(token),
task_id=f"http-{skill_name}",
workspace=granted_workspace,
)
try:
result = await agent.invoke_json(skill_name, ctx, body.arguments)
@@ -92,11 +109,43 @@ def build_app(agent: A2AAgent) -> FastAPI:
"events": [
{"kind": e.kind, "payload": e.payload} for e in ctx.events
],
"artifacts": [
{"name": name, "size_bytes": len(data)}
for name, data in (ctx.artifacts or {}).items()
],
"grant_id": grant_obj.grant_id if grant_obj else None,
}
return app
def _grant_to_workspace(grant: Grant, agent: A2AAgent) -> Any:
"""Build a :class:`WorkspaceClient` bounded by the grant.
v1 returns a :class:`LocalWorkspaceClient` whose ``access`` policy is
derived from the grant. The runtime layer (cluster service) replaces
this with a real MinIO-backed client scoped to ``grant.bucket``.
"""
from ..workspace import (
LocalWorkspaceClient,
WorkspaceAccess,
WorkspaceMode,
)
access = WorkspaceAccess.dynamic(
max_files=64,
allowed_modes=(WorkspaceMode.READ_ONLY, WorkspaceMode.READ_WRITE_OVERLAY),
require_reason=False,
deny_patterns=tuple(grant.deny_patterns),
require_human_approval=False,
)
# Empty in-memory file map; the deployed runtime substitutes a
# MinIO-backed client. See README for the cluster-side wiring.
return LocalWorkspaceClient(
files={}, access=access, bucket=grant.bucket, issuer=grant.audience
)
def serve(agent: A2AAgent, *, host: str = "0.0.0.0", port: int = 8000) -> None:
"""Run the agent's HTTP server with uvicorn (blocking)."""
import uvicorn