Add explicit source repo deploy tool
All checks were successful
build / build (push) Successful in 50s
All checks were successful
build / build (push) Successful in 50s
This commit is contained in:
@@ -35,6 +35,9 @@ The single `build(name, prompt)` skill:
|
||||
- `cp_deploy_tarball(name, version, public)` — POSTs the tarball
|
||||
to `/v1/agents/from-tarball` on the user's behalf using their
|
||||
forwarded CP JWT
|
||||
- `cp_deploy_source_repo(name)` — POSTs the current managed source repo
|
||||
to `/v1/agents/{name}/source/deploy` when source edits already live in
|
||||
Gitea
|
||||
2. Streams the graph's tool calls back to the dashboard as
|
||||
`agent_progress` events (so the user watches the build happen in
|
||||
real time, not in silence).
|
||||
|
||||
@@ -242,6 +242,11 @@ Your tools:
|
||||
elsewhere; use force=True only after
|
||||
the user explicitly accepts replacing
|
||||
the current repo source.
|
||||
- cp_deploy_source_repo(name) — deploy the current managed Gitea source
|
||||
repo without uploading MinIO files. Use
|
||||
only when source changes already live in
|
||||
the platform repo; normal builder
|
||||
workspaces should use cp_deploy_tarball.
|
||||
- cp_compose_meta_agent(name, manifest_json, description="", version="0.1.0",
|
||||
public=True, refresh_existing=False)
|
||||
— create/deploy a manifest-backed
|
||||
@@ -308,6 +313,8 @@ Discipline:
|
||||
scaffold.
|
||||
- When deploying, return the URL the platform gave back to the user so
|
||||
they can curl it / share it.
|
||||
- Source repo edits do not auto-deploy. If you edited the managed repo
|
||||
directly, call cp_deploy_source_repo after the coherent edit set is done.
|
||||
- If cp_deploy_tarball returns workspace_drift or workspace_untracked,
|
||||
use sync_agent_workspace_from_repo when the user wants the builder to
|
||||
continue from the current repo. Do not use force=True unless the user
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
- workspace file CRUD through the caller's scoped workspace grant
|
||||
- sandbox python (for `a2a card` / `a2a validate` round-trips)
|
||||
- cp_deploy_tarball (call /v1/agents/from-tarball on the user's behalf)
|
||||
- cp_deploy_source_repo (call /v1/agents/{name}/source/deploy)
|
||||
- cp_compose_meta_agent (call /v1/agents/compose with a manifest)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -661,8 +662,10 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
f"{settings.sandbox_url}/v1/run_shell",
|
||||
headers=headers,
|
||||
json={
|
||||
"bucket": bucket, "script": script,
|
||||
"image": settings.image, "memory_mib": 512,
|
||||
"bucket": bucket,
|
||||
"script": script,
|
||||
"image": settings.image,
|
||||
"memory_mib": 512,
|
||||
"timeout_seconds": settings.sandbox_timeout_s,
|
||||
},
|
||||
)
|
||||
@@ -838,6 +841,58 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
out["last_seen_version"] = live_card.get("version")
|
||||
return json.dumps(out)
|
||||
|
||||
@tool
|
||||
async def cp_deploy_source_repo(name: str) -> str:
|
||||
"""Deploy the current managed source repo for ``name``.
|
||||
|
||||
Use this when source changes already live in the platform-managed
|
||||
Gitea repo. Do not use it for normal MinIO builder workspaces; for
|
||||
those, run ``test_agent_in_sandbox`` and then ``cp_deploy_tarball``.
|
||||
|
||||
Returns JSON from the control plane, including ``deploy_id`` and a
|
||||
serialized ``deployment`` when one is queued.
|
||||
"""
|
||||
try:
|
||||
_validate_name(name)
|
||||
except ValueError as exc:
|
||||
return json.dumps({"error": str(exc)})
|
||||
if not ctx.cp_jwt:
|
||||
return json.dumps({
|
||||
"error": "no CP JWT forwarded — agent declaration missing wants_cp_jwt=True",
|
||||
})
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60.0) as c:
|
||||
r = await c.post(
|
||||
f"{settings.cp_url}/v1/agents/{name}/source/deploy",
|
||||
headers={"authorization": f"bearer {ctx.cp_jwt}"},
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
return json.dumps({"error": f"cp unreachable: {exc}"})
|
||||
if r.status_code >= 400:
|
||||
return json.dumps({"error": f"cp {r.status_code}", "detail": r.text[:1000]})
|
||||
payload = r.json()
|
||||
body = payload if isinstance(payload, dict) else {}
|
||||
deployment = body.get("deployment") if isinstance(body, dict) else None
|
||||
url = None
|
||||
if isinstance(deployment, dict):
|
||||
url = deployment.get("agent_url")
|
||||
out = {
|
||||
"ok": True,
|
||||
"name": body.get("agent_name") or name,
|
||||
"status": body.get("status") or (deployment or {}).get("status"),
|
||||
"url": url or _canonical_agent_url(name),
|
||||
"head_sha": body.get("source_sha"),
|
||||
"deployment_id": body.get("deploy_id"),
|
||||
"deployment": deployment or {},
|
||||
"already_deployed": bool(body.get("already_deployed")),
|
||||
"skipped": bool(body.get("skipped")),
|
||||
}
|
||||
if body.get("summary"):
|
||||
out["summary"] = body.get("summary")
|
||||
if body.get("reason"):
|
||||
out["reason"] = body.get("reason")
|
||||
return json.dumps(out)
|
||||
|
||||
@tool
|
||||
async def cp_compose_meta_agent(
|
||||
name: str,
|
||||
@@ -1121,7 +1176,7 @@ def build_tools(ctx: ToolContext) -> list[Any]:
|
||||
return [
|
||||
init_agent_template, list_agent_files, write_agent_file, read_agent_file,
|
||||
write_agent_skill, test_agent_in_sandbox, cp_deploy_tarball,
|
||||
cp_compose_meta_agent, cp_refresh_agent,
|
||||
cp_deploy_source_repo, cp_compose_meta_agent, cp_refresh_agent,
|
||||
sync_agent_workspace_from_repo,
|
||||
list_a2a_pack, read_a2a_pack,
|
||||
]
|
||||
@@ -1457,6 +1512,8 @@ def _compile_agent_dsl_json(bundle: bytes) -> str:
|
||||
entrypoint = str(cfg.get("entrypoint") or "").strip()
|
||||
if not entrypoint:
|
||||
raise ValueError("a2a.yaml entrypoint is required")
|
||||
entrypoint_module = entrypoint.split(":", 1)[0].strip()
|
||||
old_entrypoint_module = sys.modules.get(entrypoint_module)
|
||||
old_dont_write_bytecode = os.environ.get("PYTHONDONTWRITEBYTECODE")
|
||||
old_sys_dont_write_bytecode = sys.dont_write_bytecode
|
||||
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
@@ -1483,6 +1540,10 @@ def _compile_agent_dsl_json(bundle: bytes) -> str:
|
||||
else:
|
||||
os.environ["PYTHONDONTWRITEBYTECODE"] = old_dont_write_bytecode
|
||||
sys.dont_write_bytecode = old_sys_dont_write_bytecode
|
||||
if old_entrypoint_module is None:
|
||||
sys.modules.pop(entrypoint_module, None)
|
||||
else:
|
||||
sys.modules[entrypoint_module] = old_entrypoint_module
|
||||
return dsl.model_dump_json()
|
||||
|
||||
|
||||
|
||||
@@ -479,6 +479,64 @@ class TemplateInitTests(unittest.TestCase):
|
||||
self.assertEqual(result["error"], "sandbox_not_passed")
|
||||
self.assertEqual(_FakeAsyncClient.posts, [])
|
||||
|
||||
def test_cp_deploy_source_repo_posts_manual_source_deploy(self) -> None:
|
||||
tools = build_tools(
|
||||
ToolContext(
|
||||
bucket="bucket",
|
||||
settings=_settings(deploy_wait_timeout_s=0),
|
||||
workspace=_FakeWorkspace(),
|
||||
cp_jwt="jwt-user",
|
||||
)
|
||||
)
|
||||
deploy = _tool_by_name(tools, "cp_deploy_source_repo")
|
||||
posts: list[dict[str, object]] = []
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
text = "{}"
|
||||
|
||||
def json(self) -> dict[str, object]:
|
||||
return {
|
||||
"summary": "Queued source deployment for demo-agent",
|
||||
"agent_name": "demo-agent",
|
||||
"deploy_id": "dpl_source",
|
||||
"source_sha": "c" * 40,
|
||||
"deployment": {
|
||||
"status": "building",
|
||||
"agent_url": "https://demo-agent.a2acloud.io",
|
||||
},
|
||||
}
|
||||
|
||||
class _Client:
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
pass
|
||||
|
||||
async def __aenter__(self) -> "_Client":
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
async def post(self, url: str, *, headers: dict[str, str]) -> _Response:
|
||||
posts.append({"url": url, "headers": headers})
|
||||
return _Response()
|
||||
|
||||
with patch("agent_builder.tools.httpx.AsyncClient", _Client):
|
||||
result = json.loads(
|
||||
asyncio.run(deploy.ainvoke({"name": "demo-agent"}))
|
||||
)
|
||||
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual(result["deployment_id"], "dpl_source")
|
||||
self.assertEqual(result["head_sha"], "c" * 40)
|
||||
self.assertEqual(result["status"], "building")
|
||||
self.assertEqual(posts, [
|
||||
{
|
||||
"url": "http://cp.test/v1/agents/demo-agent/source/deploy",
|
||||
"headers": {"authorization": "bearer jwt-user"},
|
||||
}
|
||||
])
|
||||
|
||||
def test_source_bundle_hash_ignores_archive_metadata(self) -> None:
|
||||
def bundle_with_mtime(mtime: int) -> bytes:
|
||||
raw = io.BytesIO()
|
||||
|
||||
Reference in New Issue
Block a user