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

145
examples/coder_agent.py Normal file
View File

@@ -0,0 +1,145 @@
"""Example agent that drives a microsandbox VM as a general-purpose runtime.
The sandbox is **not Python-only** — agents can:
* run shell pipelines (``run_shell``)
* exec arbitrary binaries with explicit args (``handle.exec``)
* pick any OCI image (Node for codex/npx, Rust for cargo, Alpine for git, …)
The same agent class works locally on a Mac (bridge mode, libkrun) and
in-cluster once the runtime layer attaches a sandbox client to the agent's
``RunContext``.
Local run::
cd apps/a2a
pip install -e '.[dev]'
pip install -e ../sandbox-runtime'[minio]'
kubectl -n microcash-infra port-forward svc/microcash-infra-minio 9000:9000 &
A2A_MINIO_ENDPOINT=http://localhost:9000 python -m examples.coder_agent
"""
from __future__ import annotations
import asyncio
import os
from pydantic import BaseModel
from a2a_pack import A2AAgent, NoAuth, RunContext, SandboxSpec, skill
class CoderConfig(BaseModel):
default_image: str = "python:3.11-slim"
class CoderAgent(A2AAgent[CoderConfig, NoAuth]):
name = "coder-demo"
description = (
"General-purpose code-execution agent: shell, python, npm, git, etc."
)
config_model = CoderConfig
auth_model = NoAuth
tools_used = ("microsandbox", "minio")
# ----- Python-snippet shortcut --------------------------------------
@skill(description="Run inline Python and return stdout+stderr")
async def run_python(self, ctx: RunContext[NoAuth], code: str) -> str:
result = await ctx.sandbox.run_python(
code, image=self.config.default_image
)
return result.output
# ----- Arbitrary shell ---------------------------------------------
@skill(description="Run an arbitrary shell pipeline; image is overridable")
async def run_shell(
self,
ctx: RunContext[NoAuth],
script: str,
image: str | None = None,
) -> str:
result = await ctx.sandbox.run_shell(
script, image=image or self.config.default_image
)
return result.output
# ----- Multi-step session in a non-default image (codex/npm flow) ---
@skill(description="Demo: a node:20 sandbox running a small JS one-liner")
async def run_node(self, ctx: RunContext[NoAuth]) -> str:
sb = await ctx.sandbox.create(
SandboxSpec(
name="node-demo",
image="node:20-slim",
workspace="agent-coder-demo",
)
)
try:
v = await sb.exec("node", ["--version"])
r = await sb.shell(
"node -e \"console.log('sum=', [1,2,3,4].reduce((a,b)=>a+b, 0))\""
)
return f"node {v.stdout.strip()}\n{r.stdout}"
finally:
await sb.stop()
await ctx.sandbox.remove("node-demo")
# ----- See the MinIO-backed workspace from inside the VM ------------
@skill(description="ls -la /workspace from inside the sandbox")
async def list_workspace(self, ctx: RunContext[NoAuth]) -> str:
sb = await ctx.sandbox.create(
SandboxSpec(
name="ls-demo",
image=self.config.default_image,
workspace="agent-coder-demo",
)
)
try:
r = await sb.shell("ls -la /workspace")
return r.output
finally:
await sb.stop()
await ctx.sandbox.remove("ls-demo")
async def main() -> None:
# The SDK package itself stays free of microsandbox/fusepy/boto3 — the
# runtime is wired in here, at the boundary, by the host (or in cluster,
# by whoever provisions the agent's RunContext).
from sandbox_runtime import LocalMicrosandboxClient
client = LocalMicrosandboxClient(
minio_endpoint=os.environ.get("A2A_MINIO_ENDPOINT", "http://localhost:9000"),
)
agent = CoderAgent()
print("--- run_python ---")
print(
await agent.local_invoke(
"run_python",
sandbox=client,
code="import sys, platform; print('py', sys.version_info[:2], platform.machine())",
)
)
print("--- run_shell (default image) ---")
print(
await agent.local_invoke(
"run_shell",
sandbox=client,
script="cat /etc/os-release | grep PRETTY_NAME && uname -srm",
)
)
print("--- run_node (node:20-slim) ---")
print(await agent.local_invoke("run_node", sandbox=client))
print("--- list_workspace ---")
print(await agent.local_invoke("list_workspace", sandbox=client))
if __name__ == "__main__":
asyncio.run(main())

253
examples/multi_agent.py Normal file
View File

@@ -0,0 +1,253 @@
"""End-to-end demo of the platform's killer flow.
User uploads files → main agent discovers a community-published graph agent →
delegates a scoped grant → graph agent runs in its own RunContext, sees only
the granted files, returns artifacts → main agent presents results.
Two agents in the same process for the demo. In production, the graph agent
is on another pod (`HttpA2AClient`) and the discovery client hits the
control plane registry — the agent code is unchanged.
Run::
cd apps/a2a
pip install -e '.[dev]'
python -m examples.multi_agent
"""
from __future__ import annotations
import asyncio
import json
from a2a_pack import (
A2AAgent,
DiscoveredAgent,
FileType,
InMemoryA2AClient,
InMemoryDiscovery,
LocalRunContext,
LocalWorkspaceClient,
NoAuth,
RunContext,
WorkspaceAccess,
WorkspaceMode,
skill,
verify_grant,
)
# ---------------------------------------------------------------------------
# graph-agent: a community-published agent another developer wrote.
# Tags itself for discovery, declares a workspace policy.
# ---------------------------------------------------------------------------
class GraphAgent(A2AAgent):
name = "graph-agent-v2"
description = "Renders dashboards from spreadsheets"
tools_used = ("matplotlib", "pandas")
workspace_access = WorkspaceAccess.dynamic(
max_files=8,
allowed_modes=(WorkspaceMode.READ_ONLY,),
deny_patterns=("secrets/**", "**/.env"),
)
@skill(
description="Generate a dashboard from spreadsheet files",
tags=["visualization", "spreadsheet", "chart"],
)
async def generate_dashboard(
self, ctx: RunContext[NoAuth], prompt: str
) -> dict:
# Receive the workspace bound by the inbound grant. We can ONLY see
# files the caller delegated; everything else (secrets, etc.) is
# invisible.
ws = ctx.workspace
spreadsheets = await ws.search(
query="data sales revenue", types=[FileType.OTHER], limit=5
)
files_seen = [m.path for m in spreadsheets]
await ctx.emit_progress(f"reading {len(files_seen)} files for: {prompt}")
# Pretend we generated a chart. Stage outputs as artifacts; the
# platform stages them as patches under the grant's outputs prefix.
chart_bytes = (
b"\x89PNG\r\n\x1a\n" # tiny fake png prefix
+ json.dumps({"prompt": prompt, "files": files_seen}).encode()
)
ref = await ctx.write_artifact(
"charts/dashboard.png", chart_bytes, "image/png"
)
await ctx.emit_artifact(ref)
return {
"prompt": prompt,
"files_used": files_seen,
"chart": ref.uri,
"bucket_seen": getattr(ws, "bucket", None),
}
# ---------------------------------------------------------------------------
# main-agent: orchestrates the user's session. The user's full workspace is
# bound to its RunContext; it scopes a grant down to the spreadsheets only
# before calling the graph agent.
# ---------------------------------------------------------------------------
class MainAgent(A2AAgent):
name = "session-orchestrator"
description = "Routes user intents to the right specialist agent"
@skill(description="Make me a chart from my uploaded spreadsheets")
async def make_chart(self, ctx: RunContext[NoAuth], prompt: str) -> dict:
# 1. Discover: find a graph-capable agent in the registry.
candidates = await ctx.discover.find_agents(tags=["visualization"])
if not candidates:
raise RuntimeError("no graph-capable agent registered")
graph = candidates[0]
await ctx.emit_progress(f"found {graph.name}; delegating workspace")
# 2. Delegate: mint a grant scoped to *.xlsx, deny secrets, write
# to charts/ only, expires in 5 minutes.
token = await ctx.workspace.delegate(
audience=graph.name,
allow_patterns=("*.xlsx", "*.csv"),
deny_patterns=("secrets/**", "**/.env"),
outputs_prefix="charts/",
ttl_seconds=300,
)
decoded = verify_grant(token)
await ctx.emit_event_kind(
"delegation",
{
"to": graph.name,
"grant_id": decoded.grant_id,
"allow": list(decoded.allow_patterns),
"deny": list(decoded.deny_patterns),
"expires_at": decoded.expires_at,
},
) if hasattr(ctx, "emit_event_kind") else None
# 3. Call: invoke the graph agent. Runtime hands the grant in the
# body; receiving runtime materializes a workspace from it.
result = await ctx.call(
graph.name,
"generate_dashboard",
args={"prompt": prompt},
grant=token,
)
return {
"delegated_to": graph.name,
"grant_id": result.grant_id,
"graph_response": result.result,
"events_from_callee": [e["kind"] for e in result.events],
"artifacts_from_callee": list(result.artifacts),
}
# ---------------------------------------------------------------------------
# wire-up: in-memory router + discovery (replace with HTTP + control plane
# in production with zero agent-code changes)
# ---------------------------------------------------------------------------
def build_in_memory_runtime(
user_workspace: LocalWorkspaceClient, agents: dict[str, A2AAgent]
):
def factory(agent: A2AAgent, grant_token: str | None):
ws = None
if grant_token is not None:
grant = verify_grant(grant_token)
visible = {
p: b
for p, b in user_workspace._files.items()
if not any(
p.startswith((d.rstrip("*").rstrip("/")))
for d in grant.deny_patterns
if d
)
}
ws = LocalWorkspaceClient(
files=visible,
access=WorkspaceAccess.dynamic(
max_files=64,
allowed_modes=(WorkspaceMode.READ_ONLY,),
deny_patterns=tuple(grant.deny_patterns),
),
bucket=grant.bucket,
issuer=grant.audience,
)
return LocalRunContext(auth=NoAuth(), workspace=ws)
return InMemoryA2AClient(agents=agents, ctx_factory=factory)
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
async def main() -> None:
user_workspace = LocalWorkspaceClient(
files={
"sales_q1.xlsx": b"q1 data",
"sales_q2.xlsx": b"q2 data",
"notes.md": b"# notes",
"secrets/.env": b"DB_PASSWORD=NEVER",
},
access=WorkspaceAccess.dynamic(
max_files=10,
allowed_modes=(
WorkspaceMode.READ_ONLY,
WorkspaceMode.READ_WRITE_OVERLAY,
),
deny_patterns=("secrets/**",),
),
bucket="user-42-files",
issuer="user-42",
)
main_a = MainAgent()
graph_a = GraphAgent()
discovery = InMemoryDiscovery(
{
graph_a.name: DiscoveredAgent(
name=graph_a.name, url=None, card=graph_a.card()
)
}
)
a2a = build_in_memory_runtime(
user_workspace=user_workspace, agents={graph_a.name: graph_a}
)
print("=== main-agent ships uploaded files ===")
print(f" user bucket: {user_workspace.bucket}")
print(f" files: {sorted(user_workspace._files)}")
print()
out = await main_a.local_invoke(
"make_chart",
workspace=user_workspace,
a2a=a2a,
discover=discovery,
prompt="weekly burn rate by quarter",
)
print("=== main-agent result ===")
print(json.dumps(out, indent=2))
print()
print("=== what graph-agent could see ===")
print(f" bucket: {out['graph_response']['bucket_seen']}")
print(f" files_used: {out['graph_response']['files_used']}")
print(
" → secrets/.env is NOT in the list. The grant denied it; the "
"callee's runtime never made it visible."
)
if __name__ == "__main__":
asyncio.run(main())