Require sandbox pass before builder deploy
All checks were successful
build / build (push) Successful in 12s
All checks were successful
build / build (push) Successful in 12s
This commit is contained in:
@@ -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 "{}")
|
||||
|
||||
Reference in New Issue
Block a user