Require sandbox pass before builder deploy
All checks were successful
build / build (push) Successful in 12s

This commit is contained in:
robert
2026-06-07 22:53:57 -03:00
parent 49a41949ab
commit b285f3cdc8
4 changed files with 114 additions and 7 deletions

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import asyncio
import ast
import base64
import hashlib
import io
import json
import logging
@@ -465,6 +466,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
return json.dumps({
"error": "no files in agents/" + name + "/ — write some first",
})
source_hash = _source_bundle_hash(bundle_bytes)
b64 = base64.b64encode(bundle_bytes).decode("ascii")
script = (
"set -e\n"
@@ -650,6 +652,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
}
if sanitized_fields:
result["sanitized_agent_card_fields"] = list(sanitized_fields)
_record_sandbox_result(store, name, source_hash=source_hash, result=result)
return json.dumps(result)
headers = _sandbox_headers(settings.sandbox_token, ctx.grant_token)
@@ -673,6 +676,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
}
if sanitized_fields:
result["sanitized_agent_card_fields"] = list(sanitized_fields)
_record_sandbox_result(store, name, source_hash=source_hash, result=result)
return json.dumps(result)
@tool
@@ -716,11 +720,20 @@ def build_tools(ctx: ToolContext) -> list[Any]:
"error": "no CP JWT forwarded — agent declaration missing wants_cp_jwt=True",
})
builder_state = _read_builder_state(store, name)
sanitized_fields = _sanitize_agent_workspace_pricing(store, prefix)
bundle_bytes = _tarball_workspace_dir(store, prefix)
if not bundle_bytes:
return json.dumps({"error": f"no files at agents/{name}/"})
source_hash = _source_bundle_hash(bundle_bytes)
sandbox_error = _sandbox_gate_error(builder_state, source_hash=source_hash)
if sandbox_error is not None:
return json.dumps(sandbox_error)
try:
latest_deploy = await _latest_cp_deployment(settings.cp_url, ctx.cp_jwt, name)
except RuntimeError as exc:
return json.dumps({"error": str(exc)})
builder_state = _read_builder_state(store, name)
drift = _deployment_drift_error(
name,
latest_deploy,
@@ -733,10 +746,6 @@ def build_tools(ctx: ToolContext) -> list[Any]:
if force:
base_head = None
sanitized_fields = _sanitize_agent_workspace_pricing(store, prefix)
bundle_bytes = _tarball_workspace_dir(store, prefix)
if not bundle_bytes:
return json.dumps({"error": f"no files at agents/{name}/"})
# Read the entrypoint from a2a.yaml so we can pass it verbatim.
entrypoint = _read_entrypoint(bundle_bytes) or f"agent:{_class_name(name)}"
try:
@@ -1268,6 +1277,55 @@ def _write_builder_state(
store.put(_builder_state_key(name), body, "application/json")
def _source_bundle_hash(bundle: bytes) -> str:
return hashlib.sha256(bundle).hexdigest()
def _record_sandbox_result(
store: _ObjectStore,
name: str,
*,
source_hash: str,
result: dict[str, Any],
) -> None:
state = _read_builder_state(store, name)
state["last_sandbox"] = {
"source_hash": source_hash,
"exit_code": result.get("exit_code"),
"updated_at": int(time.time()),
}
_write_builder_state(store, name, state)
def _sandbox_gate_error(
builder_state: dict[str, Any],
*,
source_hash: str,
) -> dict[str, Any] | None:
sandbox = builder_state.get("last_sandbox")
if not isinstance(sandbox, dict):
return {
"ok": False,
"error": "sandbox_not_passed",
"message": "Run test_agent_in_sandbox for the current source before deploying.",
}
if sandbox.get("source_hash") != source_hash:
return {
"ok": False,
"error": "sandbox_not_passed",
"message": "Source changed after the last sandbox pass; rerun test_agent_in_sandbox.",
"last_exit_code": sandbox.get("exit_code"),
}
if sandbox.get("exit_code") != 0:
return {
"ok": False,
"error": "sandbox_not_passed",
"message": "Last sandbox run failed; fix the source and rerun test_agent_in_sandbox.",
"last_exit_code": sandbox.get("exit_code"),
}
return None
def _parse_manifest_json(raw: str) -> dict[str, Any]:
try:
parsed = json.loads(raw or "{}")