From 4bb04d81adeb76b887d9bb6f69f4ef2850434741 Mon Sep 17 00:00:00 2001 From: robert Date: Sun, 7 Jun 2026 22:32:35 -0300 Subject: [PATCH] Strengthen builder sandbox validation --- a2a.yaml | 2 +- agent.py | 2 +- agent_builder/tools.py | 139 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 2 deletions(-) diff --git a/a2a.yaml b/a2a.yaml index ee9993d..d5948ec 100644 --- a/a2a.yaml +++ b/a2a.yaml @@ -1,5 +1,5 @@ name: agent-builder -version: 0.1.4 +version: 0.1.5 entrypoint: agent:AgentBuilder description: Generate, test, and deploy new a2a agents from natural language. expose: diff --git a/agent.py b/agent.py index 268efbc..ac828b5 100644 --- a/agent.py +++ b/agent.py @@ -49,7 +49,7 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]): "Writes the project into the user's workspace, validates it in a " "sandbox, then ships it via the control plane." ) - version = "0.1.4" + version = "0.1.5" config_model = BuilderConfig auth_model = NoAuth diff --git a/agent_builder/tools.py b/agent_builder/tools.py index 3d4b04d..8183ebb 100644 --- a/agent_builder/tools.py +++ b/agent_builder/tools.py @@ -485,6 +485,145 @@ def build_tools(ctx: ToolContext) -> list[Any]: "fi\n" "echo '--- card ---'\n" "a2a card --project .\n" + "echo '--- undefined-name check ---'\n" + "python - <<'PY'\n" + "import ast, builtins, pathlib, sys\n" + "path = pathlib.Path('agent.py')\n" + "if not path.exists():\n" + " sys.exit(0)\n" + "tree = ast.parse(path.read_text(), filename=str(path))\n" + "builtins_scope = set(dir(builtins)) | {'__name__', '__file__', '__package__'}\n" + "globals_scope = set(builtins_scope)\n" + "def bind_target(target, scope):\n" + " if isinstance(target, ast.Name):\n" + " scope.add(target.id)\n" + " elif isinstance(target, (ast.Tuple, ast.List)):\n" + " for item in target.elts:\n" + " bind_target(item, scope)\n" + "for node in tree.body:\n" + " if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):\n" + " globals_scope.add(node.name)\n" + " elif isinstance(node, ast.Import):\n" + " for alias in node.names:\n" + " globals_scope.add((alias.asname or alias.name.split('.')[0]))\n" + " elif isinstance(node, ast.ImportFrom):\n" + " for alias in node.names:\n" + " if alias.name != '*':\n" + " globals_scope.add(alias.asname or alias.name)\n" + " elif isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)):\n" + " targets = getattr(node, 'targets', None) or [getattr(node, 'target', None)]\n" + " for target in targets:\n" + " bind_target(target, globals_scope)\n" + "class UndefinedNameVisitor(ast.NodeVisitor):\n" + " def __init__(self):\n" + " self.scopes = [set(globals_scope)]\n" + " self.errors = []\n" + " def _defined(self, name):\n" + " return any(name in scope for scope in reversed(self.scopes))\n" + " def _bind_args(self, args, scope):\n" + " for arg in list(args.posonlyargs) + list(args.args) + list(args.kwonlyargs):\n" + " scope.add(arg.arg)\n" + " if args.vararg:\n" + " scope.add(args.vararg.arg)\n" + " if args.kwarg:\n" + " scope.add(args.kwarg.arg)\n" + " def visit_FunctionDef(self, node):\n" + " self.visit_function(node)\n" + " def visit_AsyncFunctionDef(self, node):\n" + " self.visit_function(node)\n" + " def visit_function(self, node):\n" + " for dec in node.decorator_list:\n" + " self.visit(dec)\n" + " for default in list(node.args.defaults) + list(node.args.kw_defaults):\n" + " if default is not None:\n" + " self.visit(default)\n" + " scope = set()\n" + " self._bind_args(node.args, scope)\n" + " self.scopes.append(scope)\n" + " for stmt in node.body:\n" + " self.visit(stmt)\n" + " self.scopes.pop()\n" + " def visit_ClassDef(self, node):\n" + " for dec in node.decorator_list:\n" + " self.visit(dec)\n" + " for base in node.bases:\n" + " self.visit(base)\n" + " self.scopes.append(set())\n" + " for stmt in node.body:\n" + " self.visit(stmt)\n" + " self.scopes.pop()\n" + " def visit_Import(self, node):\n" + " for alias in node.names:\n" + " self.scopes[-1].add(alias.asname or alias.name.split('.')[0])\n" + " def visit_ImportFrom(self, node):\n" + " for alias in node.names:\n" + " if alias.name != '*':\n" + " self.scopes[-1].add(alias.asname or alias.name)\n" + " def visit_Assign(self, node):\n" + " self.visit(node.value)\n" + " for target in node.targets:\n" + " bind_target(target, self.scopes[-1])\n" + " def visit_AnnAssign(self, node):\n" + " if node.annotation:\n" + " self.visit(node.annotation)\n" + " if node.value:\n" + " self.visit(node.value)\n" + " bind_target(node.target, self.scopes[-1])\n" + " def visit_For(self, node):\n" + " self.visit(node.iter)\n" + " bind_target(node.target, self.scopes[-1])\n" + " for stmt in node.body + node.orelse:\n" + " self.visit(stmt)\n" + " def visit_AsyncFor(self, node):\n" + " self.visit_For(node)\n" + " def visit_With(self, node):\n" + " for item in node.items:\n" + " self.visit(item.context_expr)\n" + " if item.optional_vars:\n" + " bind_target(item.optional_vars, self.scopes[-1])\n" + " for stmt in node.body:\n" + " self.visit(stmt)\n" + " def visit_AsyncWith(self, node):\n" + " self.visit_With(node)\n" + " def visit_ExceptHandler(self, node):\n" + " if node.type:\n" + " self.visit(node.type)\n" + " if node.name:\n" + " self.scopes[-1].add(node.name)\n" + " for stmt in node.body:\n" + " self.visit(stmt)\n" + " def _visit_comp(self, node):\n" + " self.scopes.append(set())\n" + " for gen in node.generators:\n" + " self.visit(gen.iter)\n" + " bind_target(gen.target, self.scopes[-1])\n" + " for cond in gen.ifs:\n" + " self.visit(cond)\n" + " for field in ('elt', 'key', 'value'):\n" + " value = getattr(node, field, None)\n" + " if value is not None:\n" + " self.visit(value)\n" + " self.scopes.pop()\n" + " def visit_ListComp(self, node):\n" + " self._visit_comp(node)\n" + " def visit_SetComp(self, node):\n" + " self._visit_comp(node)\n" + " def visit_DictComp(self, node):\n" + " self._visit_comp(node)\n" + " def visit_GeneratorExp(self, node):\n" + " self._visit_comp(node)\n" + " def visit_Name(self, node):\n" + " if isinstance(node.ctx, ast.Load) and not self._defined(node.id):\n" + " self.errors.append(f'{path}:{node.lineno}:{node.col_offset}: undefined name {node.id!r}')\n" + " elif isinstance(node.ctx, (ast.Store, ast.Del)):\n" + " self.scopes[-1].add(node.id)\n" + "visitor = UndefinedNameVisitor()\n" + "visitor.visit(tree)\n" + "if visitor.errors:\n" + " print('\\n'.join(visitor.errors))\n" + " sys.exit(1)\n" + "print('ok')\n" + "PY\n" "if grep -q '^frontend:' a2a.yaml 2>/dev/null; then\n" " echo '--- frontend ---'\n" " a2a frontend info --project . || true\n"