Strengthen builder sandbox validation
All checks were successful
build / build (push) Successful in 15s

This commit is contained in:
robert
2026-06-07 22:32:35 -03:00
parent 9d3ccab800
commit 4bb04d81ad
3 changed files with 141 additions and 2 deletions

View File

@@ -1,5 +1,5 @@
name: agent-builder name: agent-builder
version: 0.1.4 version: 0.1.5
entrypoint: agent:AgentBuilder entrypoint: agent:AgentBuilder
description: Generate, test, and deploy new a2a agents from natural language. description: Generate, test, and deploy new a2a agents from natural language.
expose: expose:

View File

@@ -49,7 +49,7 @@ class AgentBuilder(A2AAgent[BuilderConfig, NoAuth]):
"Writes the project into the user's workspace, validates it in a " "Writes the project into the user's workspace, validates it in a "
"sandbox, then ships it via the control plane." "sandbox, then ships it via the control plane."
) )
version = "0.1.4" version = "0.1.5"
config_model = BuilderConfig config_model = BuilderConfig
auth_model = NoAuth auth_model = NoAuth

View File

@@ -485,6 +485,145 @@ def build_tools(ctx: ToolContext) -> list[Any]:
"fi\n" "fi\n"
"echo '--- card ---'\n" "echo '--- card ---'\n"
"a2a card --project .\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" "if grep -q '^frontend:' a2a.yaml 2>/dev/null; then\n"
" echo '--- frontend ---'\n" " echo '--- frontend ---'\n"
" a2a frontend info --project . || true\n" " a2a frontend info --project . || true\n"