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

@@ -13,6 +13,9 @@ from typing import Any, Generic, Sequence, TypeVar
from pydantic import BaseModel
from .a2a_client import A2AClient, CallResult
from .discovery import DiscoveryClient
from .sandbox import SandboxClient, SandboxUnavailable
from .workspace import WorkspaceClient
AuthT = TypeVar("AuthT", bound=BaseModel)
@@ -88,6 +91,43 @@ class RunContext(ABC, Generic[AuthT]):
Raises if the agent's :attr:`A2AAgent.workspace_access` is disabled.
"""
@property
@abstractmethod
def sandbox(self) -> SandboxClient:
"""Code-execution surface (microsandbox-backed by default).
Raises :class:`SandboxUnavailable` if the runtime did not attach a
sandbox client to this context (e.g. local dev with no host daemon).
"""
@property
@abstractmethod
def discover(self) -> DiscoveryClient:
"""Registry-backed discovery: find other agents by tag/capability/skill."""
async def call(
self,
target: str,
skill: str,
*,
args: dict[str, Any] | None = None,
grant: str | None = None,
timeout: float | None = None,
) -> CallResult:
"""Invoke another agent's skill via the runtime's :class:`A2AClient`.
``target`` is whatever the underlying client expects — an HTTP URL
for :class:`HttpA2AClient`, an agent name for in-process routing.
Pair with :meth:`WorkspaceClient.delegate` to hand a scoped
workspace grant to the callee.
"""
client = self._a2a_client()
return await client.call(target, skill, args=args, grant=grant, timeout=timeout)
@abstractmethod
def _a2a_client(self) -> A2AClient:
"""Return the runtime's outbound A2A client (or raise if absent)."""
# --- concrete helpers built on emit_event ---
async def emit_progress(self, message: str) -> None:
@@ -146,11 +186,17 @@ class LocalRunContext(RunContext[AuthT]):
task_id: str = "local-task",
secrets: dict[str, str] | None = None,
workspace: WorkspaceClient | None = None,
sandbox: SandboxClient | None = None,
a2a: A2AClient | None = None,
discover: DiscoveryClient | None = None,
) -> None:
self.task_id = task_id
self.auth = auth
self._secrets: dict[str, str] = dict(secrets or {})
self._workspace = workspace
self._sandbox = sandbox
self._a2a = a2a
self._discover = discover
self._cancel = asyncio.Event()
self.events: list[AgentEvent] = []
self.artifacts: dict[str, bytes] = {}
@@ -164,6 +210,31 @@ class LocalRunContext(RunContext[AuthT]):
)
return self._workspace
@property
def sandbox(self) -> SandboxClient:
if self._sandbox is None:
raise SandboxUnavailable(
"no sandbox client attached to this context; "
"the runtime layer must provision one"
)
return self._sandbox
@property
def discover(self) -> DiscoveryClient:
if self._discover is None:
raise PermissionError(
"no discovery client attached; runtime must provision one"
)
return self._discover
def _a2a_client(self) -> A2AClient:
if self._a2a is None:
raise PermissionError(
"no A2A client attached; runtime must provision one before "
"ctx.call(...) can be used"
)
return self._a2a
async def emit_event(self, event: AgentEvent) -> None:
self.events.append(event)