ship grants, a2a_client, discovery, sandbox SDK + tests
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user