From bc8de5a7aef783735ff6033e53bad988a406b935 Mon Sep 17 00:00:00 2001 From: robert Date: Wed, 3 Jun 2026 21:51:30 -0300 Subject: [PATCH] Forward sandbox into agent builder backend --- agent.py | 5 +++++ agent_builder/builder.py | 23 +++++++++++++++++++---- tests/test_builder_prompt.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/agent.py b/agent.py index 61a1415..b05fe91 100644 --- a/agent.py +++ b/agent.py @@ -129,11 +129,16 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]): await ctx.emit_progress( f"llm: {creds.model} via {creds.source} ({creds.base_url})" ) + try: + sandbox = ctx.sandbox + except Exception: # noqa: BLE001 - sandbox is optional in local/MCP runs. + sandbox = None graph = build_agent_builder(BuilderContext( bucket=bucket, cp_jwt=cp_jwt, project_prefix=f"agents/{name}", workspace=workspace, + sandbox=sandbox, grant_token=getattr(workspace, "_grant_token", None), llm_base_url=creds.base_url, llm_api_key=creds.api_key, diff --git a/agent_builder/builder.py b/agent_builder/builder.py index f9aee55..4e182c9 100644 --- a/agent_builder/builder.py +++ b/agent_builder/builder.py @@ -27,6 +27,7 @@ class BuilderContext: settings: Settings | None = None project_prefix: str | None = None workspace: Any | None = None + sandbox: Any | None = None grant_token: str | None = None # LLM creds forwarded by the orchestrator according to this agent's Card. # The builder accepts caller-selected creds and falls back to platform @@ -368,7 +369,12 @@ def _build_project_backend(ctx: BuilderContext, *, settings: Settings) -> Any | except Exception: # noqa: BLE001 return _with_builder_skills(StateBackend()) return _with_builder_skills( - _workspace_backend_with_grep_compat(WorkspaceBackend, ctx.workspace), + _workspace_backend_with_grep_compat( + WorkspaceBackend, + ctx.workspace, + sandbox=ctx.sandbox, + default_image=settings.image, + ), artifacts_root=f"/{prefix}/{_BUILDER_ARTIFACT_DIR}", ) try: @@ -404,12 +410,21 @@ def _build_project_backend(ctx: BuilderContext, *, settings: Settings) -> Any | ) ) return _with_builder_skills( - _workspace_backend_with_grep_compat(WorkspaceBackend, workspace), + _workspace_backend_with_grep_compat( + WorkspaceBackend, + workspace, + sandbox=ctx.sandbox, + default_image=settings.image, + ), artifacts_root=f"/{prefix}/{_BUILDER_ARTIFACT_DIR}", ) -def _workspace_backend_with_grep_compat(workspace_backend_cls: Any, workspace: Any) -> Any: +def _workspace_backend_with_grep_compat( + workspace_backend_cls: Any, + workspace: Any, + **backend_kwargs: Any, +) -> Any: """Patch older a2a-pack WorkspaceBackend grep glob behavior. Older ``WorkspaceBackend.grep`` only matched ``glob`` against the full @@ -436,7 +451,7 @@ def _workspace_backend_with_grep_compat(workspace_backend_cls: Any, workspace: A return result return super().grep(pattern, path=path, glob=scoped_glob) - return BuilderWorkspaceBackend(workspace) + return BuilderWorkspaceBackend(workspace, **backend_kwargs) def _with_builder_skills(default_backend: Any, *, artifacts_root: str = "/") -> Any: diff --git a/tests/test_builder_prompt.py b/tests/test_builder_prompt.py index c537c8d..2082ef6 100644 --- a/tests/test_builder_prompt.py +++ b/tests/test_builder_prompt.py @@ -27,6 +27,8 @@ class BuilderPromptTests(unittest.TestCase): "config={\"recursion_limit\": DEEPAGENTS_RECURSION_LIMIT}", agent_source, ) + self.assertIn("sandbox = ctx.sandbox", agent_source) + self.assertIn("sandbox=sandbox", agent_source) self.assertIn("stream=True", agent_source) self.assertIn('grant_write_prefixes=("agents/{name}/",)', agent_source) @@ -256,3 +258,37 @@ inline = Pricing(price_per_call_usd=0.01, compute={"runtime_seconds": 600}, tota result.matches, [{"path": "/agents/github-repo-inspector/agent.py"}], ) + + def test_project_backend_grep_compat_preserves_sandbox_kwargs(self) -> None: + class _Backend: + def __init__( + self, + workspace: object, + *, + sandbox: object | None = None, + default_image: str = "python:3.11-slim", + ) -> None: + self.workspace = workspace + self.sandbox = sandbox + self.default_image = default_image + + def grep( + self, + pattern: str, + path: str | None = None, + glob: str | None = None, + ): + return type("GrepResult", (), {"error": None, "matches": []})() + + workspace = object() + sandbox = object() + backend = _workspace_backend_with_grep_compat( + _Backend, + workspace, + sandbox=sandbox, + default_image="python:3.12-slim", + ) + + self.assertIs(backend.workspace, workspace) + self.assertIs(backend.sandbox, sandbox) + self.assertEqual(backend.default_image, "python:3.12-slim")