deploy
This commit is contained in:
189
.a2a/agent.dsl.json
Normal file
189
.a2a/agent.dsl.json
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "2026-06-04",
|
||||||
|
"language": "python",
|
||||||
|
"name": "self-healing-demo-v1",
|
||||||
|
"description": "A live demonstration of bounded, auditable source self-repair.",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"entrypoint": {
|
||||||
|
"module": "agent",
|
||||||
|
"class_name": "SelfHealingDemo",
|
||||||
|
"function": null,
|
||||||
|
"command": []
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"description": "Return the current self-healing demo state.",
|
||||||
|
"handler": "status",
|
||||||
|
"tags": [],
|
||||||
|
"scopes": [],
|
||||||
|
"stream": false,
|
||||||
|
"policy": {
|
||||||
|
"timeout_seconds": null,
|
||||||
|
"idempotent": false,
|
||||||
|
"max_retries": 0,
|
||||||
|
"cost_class": null,
|
||||||
|
"allow_scope_expansion": false,
|
||||||
|
"grant_mode": null,
|
||||||
|
"grant_allow_patterns": [],
|
||||||
|
"grant_deny_patterns": [],
|
||||||
|
"grant_outputs_prefix": null,
|
||||||
|
"grant_write_prefixes": [],
|
||||||
|
"grant_ttl_seconds": null,
|
||||||
|
"grant_run_timeout_seconds": null,
|
||||||
|
"grant_approval_timeout_seconds": null,
|
||||||
|
"grant_scope_approval_timeout_seconds": null
|
||||||
|
},
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"additionalProperties": true,
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "run",
|
||||||
|
"description": "Run the demo workflow, including its controlled production failure.",
|
||||||
|
"handler": "run",
|
||||||
|
"tags": [],
|
||||||
|
"scopes": [],
|
||||||
|
"stream": false,
|
||||||
|
"policy": {
|
||||||
|
"timeout_seconds": null,
|
||||||
|
"idempotent": false,
|
||||||
|
"max_retries": 0,
|
||||||
|
"cost_class": null,
|
||||||
|
"allow_scope_expansion": false,
|
||||||
|
"grant_mode": null,
|
||||||
|
"grant_allow_patterns": [],
|
||||||
|
"grant_deny_patterns": [],
|
||||||
|
"grant_outputs_prefix": null,
|
||||||
|
"grant_write_prefixes": [],
|
||||||
|
"grant_ttl_seconds": null,
|
||||||
|
"grant_run_timeout_seconds": null,
|
||||||
|
"grant_approval_timeout_seconds": null,
|
||||||
|
"grant_scope_approval_timeout_seconds": null
|
||||||
|
},
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"scenario": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"output_schema": {
|
||||||
|
"additionalProperties": true,
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"capabilities": {
|
||||||
|
"self_healing": {
|
||||||
|
"enabled": true,
|
||||||
|
"consecutive_failures": 1,
|
||||||
|
"window_seconds": 300,
|
||||||
|
"cooldown_seconds": 300,
|
||||||
|
"max_repairs_per_day": 2,
|
||||||
|
"max_turns": 30,
|
||||||
|
"deployment_timeout_seconds": 1800,
|
||||||
|
"require_tests": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"input_modes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"output_modes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"required_secrets": [],
|
||||||
|
"required_env": [],
|
||||||
|
"consumer_setup": {
|
||||||
|
"fields": []
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lifecycle": "ephemeral",
|
||||||
|
"availability": "always_on",
|
||||||
|
"state": "none",
|
||||||
|
"sandbox": "microsandbox",
|
||||||
|
"resources": {
|
||||||
|
"cpu": "100m",
|
||||||
|
"memory": "256Mi",
|
||||||
|
"gpu": 0,
|
||||||
|
"max_runtime_seconds": 120
|
||||||
|
},
|
||||||
|
"concurrency": 1,
|
||||||
|
"egress": {
|
||||||
|
"allow_hosts": [],
|
||||||
|
"allow_internal_services": [],
|
||||||
|
"deny_internet_by_default": true
|
||||||
|
},
|
||||||
|
"tools_used": [],
|
||||||
|
"llm_provisioning": "platform",
|
||||||
|
"pricing": {
|
||||||
|
"price_per_call_usd": 0.0,
|
||||||
|
"caller_pays_llm": false,
|
||||||
|
"notes": ""
|
||||||
|
},
|
||||||
|
"wants_cp_jwt": false,
|
||||||
|
"platform_resources": {
|
||||||
|
"memory": null,
|
||||||
|
"databases": [],
|
||||||
|
"mailbox": null
|
||||||
|
},
|
||||||
|
"endpoints": [],
|
||||||
|
"apt_packages": []
|
||||||
|
},
|
||||||
|
"template_lineage": null,
|
||||||
|
"meta_agent_manifest": null,
|
||||||
|
"state_schema": null,
|
||||||
|
"workspace_access": {
|
||||||
|
"enabled": false,
|
||||||
|
"max_files": 0,
|
||||||
|
"allowed_modes": [],
|
||||||
|
"require_reason": true,
|
||||||
|
"deny_patterns": [],
|
||||||
|
"require_human_approval": false,
|
||||||
|
"max_total_size_bytes": 104857600
|
||||||
|
},
|
||||||
|
"config_schema": {
|
||||||
|
"description": "Default config model when an agent declares no config.",
|
||||||
|
"properties": {},
|
||||||
|
"title": "_EmptyConfig",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"model": "a2a_pack.auth.NoAuth",
|
||||||
|
"strategy": "public",
|
||||||
|
"principal_schema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "Public agent: no caller identity required.",
|
||||||
|
"properties": {},
|
||||||
|
"title": "NoAuth",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"resolver": null,
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"source": "python-a2a-pack",
|
||||||
|
"project_manifest": {
|
||||||
|
"name": "self-healing-demo-v1",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"entrypoint": "agent:SelfHealingDemo",
|
||||||
|
"frontend": {
|
||||||
|
"path": "frontend",
|
||||||
|
"build": "mkdir -p dist && cp index.html app.js styles.css dist/",
|
||||||
|
"dist": "dist",
|
||||||
|
"mount": "/",
|
||||||
|
"auth": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
a2a.yaml
Normal file
27
a2a.yaml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: self-healing-demo-v1
|
||||||
|
description: A live demonstration of bounded, auditable source self-repair.
|
||||||
|
version: 0.1.0
|
||||||
|
entrypoint: agent:SelfHealingDemo
|
||||||
|
expose:
|
||||||
|
public: true
|
||||||
|
self_healing:
|
||||||
|
enabled: true
|
||||||
|
consecutive_failures: 1
|
||||||
|
window_seconds: 300
|
||||||
|
cooldown_seconds: 300
|
||||||
|
max_repairs_per_day: 2
|
||||||
|
max_turns: 30
|
||||||
|
deployment_timeout_seconds: 1800
|
||||||
|
require_tests: true
|
||||||
|
runtime:
|
||||||
|
availability: always_on
|
||||||
|
resources:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
max_runtime_seconds: 120
|
||||||
|
frontend:
|
||||||
|
path: frontend
|
||||||
|
build: mkdir -p dist && cp index.html app.js styles.css dist/
|
||||||
|
dist: dist
|
||||||
|
mount: /
|
||||||
|
auth: public
|
||||||
29
agent.py
Normal file
29
agent.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from a2a_pack import A2AAgent, NoAuth, RunContext, skill
|
||||||
|
|
||||||
|
|
||||||
|
class SelfHealingDemo(A2AAgent):
|
||||||
|
name = "self-healing-demo-v1"
|
||||||
|
description = "A live demonstration of bounded, auditable source self-repair."
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
@skill(description="Return the current self-healing demo state.")
|
||||||
|
async def status(self, ctx: RunContext[NoAuth]) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"agent": self.name,
|
||||||
|
"message": "The agent is online. Trigger the controlled failure once to exercise repair.",
|
||||||
|
}
|
||||||
|
|
||||||
|
@skill(description="Run the demo workflow, including its controlled production failure.")
|
||||||
|
async def run(
|
||||||
|
self,
|
||||||
|
ctx: RunContext[NoAuth],
|
||||||
|
scenario: str = "normal",
|
||||||
|
) -> dict[str, object]:
|
||||||
|
if scenario == "controlled-failure":
|
||||||
|
# Intentional smoke fixture: the regression test specifies the safe
|
||||||
|
# result. The production self-healing worker must repair this source.
|
||||||
|
raise RuntimeError("SELF_HEAL_SMOKE: controlled invoice total handler crashed")
|
||||||
|
return {"ok": True, "scenario": scenario, "result": "healthy"}
|
||||||
17
frontend/app.js
Normal file
17
frontend/app.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { agent } from "/a2a-client.js";
|
||||||
|
|
||||||
|
const button = document.querySelector("#trigger");
|
||||||
|
const result = document.querySelector("#result");
|
||||||
|
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
button.disabled = true;
|
||||||
|
result.textContent = "Calling run(scenario=controlled-failure)…";
|
||||||
|
try {
|
||||||
|
const response = await agent.call("run", { scenario: "controlled-failure" });
|
||||||
|
result.textContent = JSON.stringify(response, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
result.textContent = `Failure observed. The bounded repair should now be queued.\n\n${error.message}`;
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
36
frontend/index.html
Normal file
36
frontend/index.html
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="description" content="A live A2A Cloud self-healing agent demonstration." />
|
||||||
|
<title>Self-Healing Agent · A2A Cloud</title>
|
||||||
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="eyebrow"><span></span> LIVE PLATFORM DEMO</div>
|
||||||
|
<h1>Software that repairs<br /><em>its own runtime failure.</em></h1>
|
||||||
|
<p class="lede">
|
||||||
|
This agent opts in through <code>a2a.yaml</code>. A real 500 error creates a bounded repair job,
|
||||||
|
patches managed source, runs tests, and redeploys the exact commit—with the full trail in the app.
|
||||||
|
</p>
|
||||||
|
<section class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<div><small>SELF-HEALING POLICY</small><strong>Enabled</strong></div>
|
||||||
|
<div class="pulse">READY</div>
|
||||||
|
</div>
|
||||||
|
<div class="policy">
|
||||||
|
<div><span>Trigger</span><b>1 internal failure</b></div>
|
||||||
|
<div><span>Cooldown</span><b>5 minutes</b></div>
|
||||||
|
<div><span>Daily ceiling</span><b>2 repairs</b></div>
|
||||||
|
<div><span>Promotion gate</span><b>Tests + exact SHA live</b></div>
|
||||||
|
</div>
|
||||||
|
<button id="trigger">Trigger controlled failure</button>
|
||||||
|
<pre id="result">Waiting for a test run.</pre>
|
||||||
|
</section>
|
||||||
|
<p class="foot">Expected input and 4xx errors never mutate source. Credentials and user inputs stay out of repair evidence.</p>
|
||||||
|
</main>
|
||||||
|
<script type="module" src="./app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
frontend/styles.css
Normal file
24
frontend/styles.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; background: #090b10; color: #edf1f7; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 78% 15%, #16362d 0, transparent 30%), linear-gradient(145deg, #090b10, #0d1218); }
|
||||||
|
main { width: min(920px, calc(100% - 40px)); margin: 0 auto; padding: 9vh 0 8vh; }
|
||||||
|
.eyebrow { color: #7f8b9c; font-size: 11px; letter-spacing: .2em; display: flex; align-items: center; gap: 10px; }
|
||||||
|
.eyebrow span { width: 8px; height: 8px; border-radius: 99px; background: #5dffb2; box-shadow: 0 0 22px #5dffb2; }
|
||||||
|
h1 { margin: 28px 0 22px; font-size: clamp(48px, 8vw, 88px); line-height: .98; letter-spacing: -.055em; font-weight: 580; }
|
||||||
|
h1 em { font-style: normal; color: #76efb5; }
|
||||||
|
.lede { max-width: 760px; color: #aab4c2; font-size: clamp(17px, 2.2vw, 21px); line-height: 1.58; }
|
||||||
|
code { color: #76efb5; }
|
||||||
|
.card { margin-top: 48px; padding: 28px; border: 1px solid #27323e; border-radius: 18px; background: rgba(14, 18, 25, .82); box-shadow: 0 26px 80px #0008; }
|
||||||
|
.card-head { display: flex; justify-content: space-between; gap: 20px; align-items: flex-start; }
|
||||||
|
.card-head small { display: block; color: #728094; letter-spacing: .14em; }
|
||||||
|
.card-head strong { display: block; margin-top: 8px; font-size: 28px; }
|
||||||
|
.pulse { color: #67f6ad; border: 1px solid #32634d; background: #10281f; border-radius: 99px; padding: 8px 12px; font: 11px ui-monospace, monospace; }
|
||||||
|
.policy { margin: 26px 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border: 1px solid #202a35; border-radius: 12px; overflow: hidden; }
|
||||||
|
.policy div { padding: 16px; border: 1px solid #202a35; }
|
||||||
|
.policy span { display: block; color: #728094; font-size: 11px; text-transform: uppercase; letter-spacing: .1em; }
|
||||||
|
.policy b { display: block; margin-top: 7px; font-size: 14px; }
|
||||||
|
button { width: 100%; border: 0; border-radius: 10px; padding: 15px 18px; background: #70efb2; color: #07120d; font: 700 14px ui-monospace, monospace; cursor: pointer; }
|
||||||
|
button:disabled { opacity: .5; cursor: wait; }
|
||||||
|
pre { min-height: 74px; margin: 14px 0 0; padding: 16px; border-radius: 10px; background: #080a0e; color: #96a4b7; white-space: pre-wrap; overflow: auto; font-size: 12px; }
|
||||||
|
.foot { color: #687588; font-size: 12px; margin-top: 20px; }
|
||||||
|
@media (max-width: 620px) { .policy { grid-template-columns: 1fr; } .card { padding: 20px; } }
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
a2a-pack==0.1.113
|
||||||
|
pytest>=8
|
||||||
|
pytest-asyncio>=0.23
|
||||||
21
tests/test_agent.py
Normal file
21
tests/test_agent.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from a2a_pack import LocalRunContext, NoAuth
|
||||||
|
|
||||||
|
from agent import SelfHealingDemo
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_controlled_failure_has_a_safe_result() -> None:
|
||||||
|
agent = SelfHealingDemo()
|
||||||
|
ctx = LocalRunContext(auth=NoAuth())
|
||||||
|
|
||||||
|
result = await agent.run(ctx, scenario="controlled-failure")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"ok": True,
|
||||||
|
"scenario": "controlled-failure",
|
||||||
|
"result": "recovered",
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user