Use sandbox client for builder validation
All checks were successful
build / build (push) Successful in 14s

This commit is contained in:
robert
2026-06-06 20:24:38 -03:00
parent 5d03074fa4
commit 137f84be6a
11 changed files with 92 additions and 29 deletions

View File

@@ -298,4 +298,4 @@ def _build_failure_error(exc: BaseException) -> str:
"or a smaller build scope" "or a smaller build scope"
) )
return f"build graph failed: {type(exc).__name__}: {exc}" return f"build graph failed: {type(exc).__name__}: {exc}"
# rebuild against a2a-pack 0.1.60 for source-grant-aware agent updates # rebuild against a2a-pack 0.1.75 for source-grant-aware agent updates

View File

@@ -321,6 +321,7 @@ def build_agent_builder(ctx: BuilderContext) -> Any:
settings=settings, settings=settings,
cp_jwt=ctx.cp_jwt, cp_jwt=ctx.cp_jwt,
workspace=ctx.workspace, workspace=ctx.workspace,
sandbox=ctx.sandbox,
grant_token=ctx.grant_token, grant_token=ctx.grant_token,
) )
) )

View File

@@ -29,7 +29,7 @@ Reserve `LLMProvisioning.PLATFORM_OR_CALLER_PROVIDED` for legacy compatibility.
In every mode, read `ctx.llm`; In every mode, read `ctx.llm`;
never read `A2A_LITELLM_KEY`, `OPENAI_API_KEY`, provider keys, or platform never read `A2A_LITELLM_KEY`, `OPENAI_API_KEY`, provider keys, or platform
secrets directly. If `ctx.llm.api_key` is empty, return a clear setup/config secrets directly. If `ctx.llm.api_key` is empty, return a clear setup/config
result before constructing `ChatOpenAI`. result before constructing a model or calling `create_a2a_deep_agent`.
`Pricing` only accepts `price_per_call_usd`, `caller_pays_llm`, and `notes`. `Pricing` only accepts `price_per_call_usd`, `caller_pays_llm`, and `notes`.
Do not put `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, Do not put `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`,

View File

@@ -41,8 +41,8 @@ Check these items:
- `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`. - `Pricing` contains only `price_per_call_usd`, `caller_pays_llm`, and `notes`.
Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or Reject `runtime.pricing.compute`, `runtime.pricing.total_usd`, `compute=`, or
`total_usd=` in `agent.py`; those are derived platform fields. `total_usd=` in `agent.py`; those are derived platform fields.
- Any code path that builds `ChatOpenAI` checks `ctx.llm.api_key` first or - DeepAgents code uses `create_a2a_deep_agent` or another `ctx.llm`-backed
returns a clear setup/config result before the model constructor runs. resolver, and checks `ctx.llm.api_key` before model construction.
- Any use of DeepAgents file tools passes `backend=ctx.workspace_backend()`. - Any use of DeepAgents file tools passes `backend=ctx.workspace_backend()`.
- Any project skills are seeded into the backend and passed with - Any project skills are seeded into the backend and passed with
`skills=skill_sources or None`. `skills=skill_sources or None`.

View File

@@ -1,6 +1,6 @@
--- ---
name: deepagent-agent-design name: deepagent-agent-design
description: Design generated A2A agents around DeepAgents skills, subagents, durable workspace backends, and ctx.llm-backed reasoning. Use whenever building or modifying an agent, deciding whether to add tools, creating skill bundles, wiring create_deep_agent, or avoiding shallow fake tools. description: Design generated A2A agents around DeepAgents skills, subagents, durable workspace backends, and ctx.llm-backed reasoning. Use whenever building or modifying an agent, deciding whether to add tools, creating skill bundles, wiring create_a2a_deep_agent, or avoiding shallow fake tools.
--- ---
# DeepAgent Agent Design # DeepAgent Agent Design
@@ -13,8 +13,8 @@ planning, file work, skill selection, and subagent delegation.
Use this shape for non-trivial generated agents: Use this shape for non-trivial generated agents:
1. Keep one or two public A2A `@skill` methods with clear typed parameters. 1. Keep one or two public A2A `@skill` methods with clear typed parameters.
2. Inside each method, read `ctx.llm`, verify credentials are available before 2. Inside each method, read `ctx.llm`, verify credentials are available, and
constructing `ChatOpenAI`, and build a DeepAgent with `create_deep_agent`. build a DeepAgent with `create_a2a_deep_agent`.
3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the 3. Pass `backend=ctx.workspace_backend()` so DeepAgents file tools write to the
caller's durable workspace instead of LangGraph state. caller's durable workspace instead of LangGraph state.
4. Seed project skills into the current grant's write prefix and pass 4. Seed project skills into the current grant's write prefix and pass
@@ -44,7 +44,7 @@ linked from `SKILL.md`.
Project `skills/` files are source files in the agent image. DeepAgents loads Project `skills/` files are source files in the agent image. DeepAgents loads
skills from its backend, so seed those files into the invocation workspace skills from its backend, so seed those files into the invocation workspace
before `create_deep_agent`: before `create_a2a_deep_agent`:
```python ```python
from pathlib import Path from pathlib import Path
@@ -84,8 +84,9 @@ Then:
```python ```python
backend = ctx.workspace_backend() backend = ctx.workspace_backend()
skill_sources = _seed_runtime_skills(backend, ctx) skill_sources = _seed_runtime_skills(backend, ctx)
return create_deep_agent( return create_a2a_deep_agent(
model=model, ctx,
creds=creds,
backend=backend, backend=backend,
skills=skill_sources or None, skills=skill_sources or None,
tools=[small_deterministic_tool], tools=[small_deterministic_tool],

View File

@@ -1,12 +1,12 @@
--- ---
name: deepagents-implementation-patterns name: deepagents-implementation-patterns
description: Implement DeepAgents inside a2a-pack agents with real deterministic tools, project skills, custom subagents, workspace-backed files, and ctx.llm-backed ChatOpenAI models. Use when editing create_deep_agent calls or deciding what belongs in tools, skills, or subagents. description: Implement DeepAgents inside a2a-pack agents with real deterministic tools, project skills, custom subagents, workspace-backed files, and ctx.llm-backed model resolution. Use when editing create_a2a_deep_agent calls or deciding what belongs in tools, skills, or subagents.
--- ---
# DeepAgents Implementation Patterns # DeepAgents Implementation Patterns
DeepAgents supplies the inner agent loop. In this platform, the reliable shape DeepAgents supplies the inner agent loop. In this platform, the reliable shape
is: `ctx.llm`-backed `ChatOpenAI`, A2A workspace backend, project DeepAgents is: `ctx.llm`-backed `create_a2a_deep_agent`, A2A workspace backend, project
skills, and a small number of exact tools. Hosted generated agents should DeepAgents skills, and a small number of exact tools. Hosted generated agents should
declare `LLMProvisioning.PLATFORM`; explicit BYOK agents can declare declare `LLMProvisioning.PLATFORM`; explicit BYOK agents can declare
`CALLER_PROVIDED`, but both modes still read the same `ctx.llm` object. `CALLER_PROVIDED`, but both modes still read the same `ctx.llm` object.
@@ -20,8 +20,7 @@ from pathlib import Path
from typing import Any from typing import Any
from langchain_core.tools import tool from langchain_core.tools import tool
from langchain_openai import ChatOpenAI from a2a_pack.deepagents import create_a2a_deep_agent
from deepagents import create_deep_agent
RUNTIME_SKILLS_DIR = "research-agent/.deepagents/skills/" RUNTIME_SKILLS_DIR = "research-agent/.deepagents/skills/"
@@ -39,13 +38,6 @@ def build_graph(ctx: Any) -> Any:
"LLM credentials were not available; declare PLATFORM for hosted " "LLM credentials were not available; declare PLATFORM for hosted "
"generated agents or configure caller-provided credentials." "generated agents or configure caller-provided credentials."
) )
model = ChatOpenAI(
model=creds.model,
base_url=creds.base_url,
api_key=creds.api_key,
temperature=0.0,
)
@tool @tool
def validate_citations(items_json: str) -> str: def validate_citations(items_json: str) -> str:
"""Validate that each citation has title, url, and claim fields.""" """Validate that each citation has title, url, and claim fields."""
@@ -58,8 +50,9 @@ def build_graph(ctx: Any) -> Any:
backend = ctx.workspace_backend() backend = ctx.workspace_backend()
skill_sources = seed_runtime_skills(backend, ctx) skill_sources = seed_runtime_skills(backend, ctx)
return create_deep_agent( return create_a2a_deep_agent(
model=model, ctx,
creds=creds,
backend=backend, backend=backend,
skills=skill_sources or None, skills=skill_sources or None,
tools=[validate_citations], tools=[validate_citations],

View File

@@ -26,7 +26,7 @@ Pass the backend into DeepAgents:
```python ```python
backend = ctx.workspace_backend() backend = ctx.workspace_backend()
graph = create_deep_agent(model=model, backend=backend, skills=skill_sources or None) graph = create_a2a_deep_agent(ctx, backend=backend, skills=skill_sources or None)
``` ```
For stable, human-readable paths, ask model-created files to use For stable, human-readable paths, ask model-created files to use

View File

@@ -31,7 +31,7 @@ if TYPE_CHECKING:
from .config import Settings from .config import Settings
A2A_PACK_MIN_VERSION = "0.1.60" A2A_PACK_MIN_VERSION = "0.1.75"
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -40,6 +40,7 @@ class ToolContext:
settings: "Settings" settings: "Settings"
cp_jwt: str | None = None cp_jwt: str | None = None
workspace: Any | None = None workspace: Any | None = None
sandbox: Any | None = None
grant_token: str | None = None grant_token: str | None = None
@@ -444,6 +445,29 @@ def build_tools(ctx: ToolContext) -> list[Any]:
" a2a frontend info --project . || true\n" " a2a frontend info --project . || true\n"
"fi\n" "fi\n"
) )
if ctx.sandbox is not None:
try:
out = await ctx.sandbox.run_shell(
script,
image=settings.image,
workspace=bucket,
memory_mib=512,
timeout_seconds=settings.sandbox_timeout_s,
)
except Exception as exc: # noqa: BLE001
return json.dumps({
"error": f"sandbox {type(exc).__name__}",
"detail": str(exc)[:1000],
})
result: dict[str, Any] = {
"exit_code": getattr(out, "exit_code", None),
"stdout": (getattr(out, "stdout", "") or "")[:4000],
"stderr": (getattr(out, "stderr", "") or "")[:4000],
}
if sanitized_fields:
result["sanitized_agent_card_fields"] = list(sanitized_fields)
return json.dumps(result)
headers = _sandbox_headers(settings.sandbox_token, ctx.grant_token) headers = _sandbox_headers(settings.sandbox_token, ctx.grant_token)
async with httpx.AsyncClient(timeout=settings.sandbox_timeout_s + 30) as c: async with httpx.AsyncClient(timeout=settings.sandbox_timeout_s + 30) as c:
r = await c.post( r = await c.post(

View File

@@ -1,4 +1,4 @@
a2a-pack>=0.1.60 a2a-pack>=0.1.75
httpx>=0.27 httpx>=0.27
boto3>=1.34 boto3>=1.34
deepagents>=0.5.0 deepagents>=0.5.0

View File

@@ -162,7 +162,7 @@ inline = Pricing(price_per_call_usd=0.01, compute={"runtime_seconds": 600}, tota
self.assertIn("skills=skill_sources or None", files["deepagent-agent-design/SKILL.md"]) self.assertIn("skills=skill_sources or None", files["deepagent-agent-design/SKILL.md"])
self.assertIn("from a2a_pack import", files["a2apack-agent-authoring/SKILL.md"]) self.assertIn("from a2a_pack import", files["a2apack-agent-authoring/SKILL.md"])
self.assertIn("ctx.workspace_shell", files["a2apack-agent-authoring/SKILL.md"]) self.assertIn("ctx.workspace_shell", files["a2apack-agent-authoring/SKILL.md"])
self.assertIn("create_deep_agent", files["deepagents-implementation-patterns/SKILL.md"]) self.assertIn("create_a2a_deep_agent", files["deepagents-implementation-patterns/SKILL.md"])
self.assertIn('config={"recursion_limit": 500}', files["deepagents-implementation-patterns/SKILL.md"]) self.assertIn('config={"recursion_limit": 500}', files["deepagents-implementation-patterns/SKILL.md"])
self.assertIn("ctx.write_artifact", files["workspace-artifact-safety/SKILL.md"]) self.assertIn("ctx.write_artifact", files["workspace-artifact-safety/SKILL.md"])
self.assertIn("ctx.workspace_shell", files["workspace-artifact-safety/SKILL.md"]) self.assertIn("ctx.workspace_shell", files["workspace-artifact-safety/SKILL.md"])

View File

@@ -47,7 +47,7 @@ class TemplateInitTests(unittest.TestCase):
self.assertIn("skills=skill_sources or None", files["agent.py"]) self.assertIn("skills=skill_sources or None", files["agent.py"])
self.assertIn("ctx.llm", files["agent.py"]) self.assertIn("ctx.llm", files["agent.py"])
self.assertIn("ctx.workspace_backend()", files["agent.py"]) self.assertIn("ctx.workspace_backend()", files["agent.py"])
self.assertIn("create_deep_agent", files["agent.py"]) self.assertIn("create_a2a_deep_agent", files["agent.py"])
self.assertIn("name: research-agent", files["a2a.yaml"]) self.assertIn("name: research-agent", files["a2a.yaml"])
self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"]) self.assertIn("entrypoint: agent:ResearchAgent", files["a2a.yaml"])
self.assertNotIn("{{ frontend_block }}", files["a2a.yaml"]) self.assertNotIn("{{ frontend_block }}", files["a2a.yaml"])
@@ -272,6 +272,35 @@ class TemplateInitTests(unittest.TestCase):
self.assertIn("manifest_json must be a JSON object", result["error"]) self.assertIn("manifest_json must be a JSON object", result["error"])
def test_test_agent_in_sandbox_uses_attached_sandbox_client(self) -> None:
workspace = _FakeWorkspace()
prefix = "agents/demo-agent/"
workspace.write_bytes(prefix + "agent.py", b"print('ok')\n")
workspace.write_bytes(prefix + "a2a.yaml", b"name: demo-agent\n")
workspace.write_bytes(prefix + "requirements.txt", b"")
sandbox = _FakeSandbox()
tools = build_tools(
ToolContext(
bucket="bucket",
settings=_settings(),
workspace=workspace,
sandbox=sandbox,
grant_token="grant-token",
)
)
test_tool = _tool_by_name(tools, "test_agent_in_sandbox")
_FakeAsyncClient.posts = []
with patch("agent_builder.tools.httpx.AsyncClient", _FakeAsyncClient):
result = json.loads(
asyncio.run(test_tool.ainvoke({"name": "demo-agent"}))
)
self.assertEqual(result["exit_code"], 0)
self.assertEqual(len(sandbox.calls), 1)
self.assertEqual(sandbox.calls[0]["workspace"], "bucket")
self.assertEqual(_FakeAsyncClient.posts, [])
def test_cp_deploy_tarball_posts_agent_dsl(self) -> None: def test_cp_deploy_tarball_posts_agent_dsl(self) -> None:
workspace = _FakeWorkspace() workspace = _FakeWorkspace()
prefix = "agents/demo-agent/" prefix = "agents/demo-agent/"
@@ -529,6 +558,21 @@ class _FakeWorkspace:
self.objects.pop(key, None) self.objects.pop(key, None)
class _FakeExecResult:
exit_code = 0
stdout = "--- card ---\n{}"
stderr = ""
class _FakeSandbox:
def __init__(self) -> None:
self.calls: list[dict[str, object]] = []
async def run_shell(self, script: str, **kwargs: object) -> _FakeExecResult:
self.calls.append({"script": script, **kwargs})
return _FakeExecResult()
def _tool_by_name(tools: list[object], name: str) -> object: def _tool_by_name(tools: list[object], name: str) -> object:
for tool in tools: for tool in tools:
if getattr(tool, "name", None) == name: if getattr(tool, "name", None) == name: