snapback-selfheal 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- snapback_selfheal-0.1.0/PKG-INFO +87 -0
- snapback_selfheal-0.1.0/README.md +74 -0
- snapback_selfheal-0.1.0/pyproject.toml +25 -0
- snapback_selfheal-0.1.0/setup.cfg +4 -0
- snapback_selfheal-0.1.0/snapback_interceptor.py +194 -0
- snapback_selfheal-0.1.0/snapback_selfheal.egg-info/PKG-INFO +87 -0
- snapback_selfheal-0.1.0/snapback_selfheal.egg-info/SOURCES.txt +7 -0
- snapback_selfheal-0.1.0/snapback_selfheal.egg-info/dependency_links.txt +1 -0
- snapback_selfheal-0.1.0/snapback_selfheal.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snapback-selfheal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Make your AI agent self-heal — auto-diagnose every error and apply safe fixes, gated. Snapback interceptor for OpenClaw and any Python agent.
|
|
5
|
+
Author: Snapback / RA1 Labs
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://snapback.sh
|
|
8
|
+
Project-URL: For agents, https://snapback.sh/for-agents
|
|
9
|
+
Project-URL: Docs, https://snapback.sh/docs
|
|
10
|
+
Keywords: ai-agent,self-healing,agentops,openclaw,error-recovery,mcp,snapback
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Snapback self-heal interceptor for OpenClaw
|
|
15
|
+
|
|
16
|
+
**Make Snapback automatic.** Instead of remembering to call the diagnosis tool, wrap your agent's tool calls
|
|
17
|
+
once — and every error is auto-diagnosed, and gated-safe fixes are applied and retried *without a human*.
|
|
18
|
+
|
|
19
|
+
> "Power tools are optional. Infrastructure is not." This turns Snapback from a tool you reach for into
|
|
20
|
+
> infrastructure that runs on every failure.
|
|
21
|
+
|
|
22
|
+
## What it does
|
|
23
|
+
On **any** tool-call error, the interceptor:
|
|
24
|
+
1. Calls `diagnose_infra_error` (free, no token, no LLM, <150ms).
|
|
25
|
+
2. Applies the **three-factor gate** — auto-apply a fix only when **all** are true:
|
|
26
|
+
- `confidence >= 0.85`
|
|
27
|
+
- `source == "library"` (a curated verified fix, not an LLM guess)
|
|
28
|
+
- `auto_safe == true` (the fix is `retry` / `refetch` / `config` — reversible, side-effect-free)
|
|
29
|
+
3. If the gate passes → applies the fix and **retries once**. If it fails → **escalates to a human** (logs the
|
|
30
|
+
verdict). It **never** auto-applies a `mutate` or `destructive` fix (create/change state, money, auth grants).
|
|
31
|
+
|
|
32
|
+
It fails **closed**: when unsure, it escalates rather than acting.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
```bash
|
|
36
|
+
# no install needed beyond the stdlib; drop the file in, or:
|
|
37
|
+
cp snapback_interceptor.py your_agent/
|
|
38
|
+
export SNAPBACK_MCP=https://api.snapback.sh/mcp # default; the free tools need no token
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use
|
|
42
|
+
```python
|
|
43
|
+
from snapback_interceptor import SnapbackInterceptor
|
|
44
|
+
|
|
45
|
+
heal = SnapbackInterceptor(
|
|
46
|
+
auto_apply=True,
|
|
47
|
+
on_escalate=lambda v: my_ops_inbox.log(v), # human sees non-auto-safe verdicts
|
|
48
|
+
on_heal=lambda v: metrics.count("snapback.autoheal", family=v["family"]),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# wrap any call that might raise:
|
|
52
|
+
result = heal.run(call_solana_rpc, wallet, amount)
|
|
53
|
+
|
|
54
|
+
# or as a decorator:
|
|
55
|
+
@heal.guard
|
|
56
|
+
def call_solana_rpc(wallet, amount): ...
|
|
57
|
+
|
|
58
|
+
# or manually on a caught error:
|
|
59
|
+
verdict = heal.diagnose("BlockhashNotFound")
|
|
60
|
+
if verdict["gate"]["auto_apply_ok"]:
|
|
61
|
+
...apply verdict["fix"] and retry...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## The gate contract (returned by Snapback)
|
|
65
|
+
`diagnose_infra_error` returns `{matched, family, fix, confidence, source, action_class, auto_safe, gate}`.
|
|
66
|
+
`action_class` is one of:
|
|
67
|
+
|
|
68
|
+
| class | meaning | auto-safe? |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `retry` | re-run with backoff (idempotent) | ✅ |
|
|
71
|
+
| `refetch` | re-fetch/re-price/re-sync then re-apply | ✅ |
|
|
72
|
+
| `config` | client-side param change (raise max_tokens, serve intermediate cert) | ✅ (to suggest) |
|
|
73
|
+
| `mutate` | creates/changes external state, money, auth | ❌ escalate |
|
|
74
|
+
| `destructive` | deletes/reverts/irreversible | ❌ never |
|
|
75
|
+
|
|
76
|
+
`gate.auto_apply_ok` is the ready-made verdict; the interceptor also re-derives it locally so a stale client
|
|
77
|
+
can't over-trust.
|
|
78
|
+
|
|
79
|
+
## Privacy & safety
|
|
80
|
+
- 100% client-side. Talks only to the free public `diagnose_infra_error` endpoint.
|
|
81
|
+
- Never sends your Snapback token for the diagnosis (diagnosis is free).
|
|
82
|
+
- Snapback scrubs trace content server-side; this sends only the error string you pass.
|
|
83
|
+
- Composes with `bridge.py` (post-mortem forwarding) — this is the live auto-heal layer.
|
|
84
|
+
|
|
85
|
+
## Roadmap
|
|
86
|
+
- `submit_feedback` on the retry outcome (did the fix work?) → feeds the shared library (the network effect).
|
|
87
|
+
- Hermes + LangChain adapters (same gate, different framework hook).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Snapback self-heal interceptor for OpenClaw
|
|
2
|
+
|
|
3
|
+
**Make Snapback automatic.** Instead of remembering to call the diagnosis tool, wrap your agent's tool calls
|
|
4
|
+
once — and every error is auto-diagnosed, and gated-safe fixes are applied and retried *without a human*.
|
|
5
|
+
|
|
6
|
+
> "Power tools are optional. Infrastructure is not." This turns Snapback from a tool you reach for into
|
|
7
|
+
> infrastructure that runs on every failure.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
On **any** tool-call error, the interceptor:
|
|
11
|
+
1. Calls `diagnose_infra_error` (free, no token, no LLM, <150ms).
|
|
12
|
+
2. Applies the **three-factor gate** — auto-apply a fix only when **all** are true:
|
|
13
|
+
- `confidence >= 0.85`
|
|
14
|
+
- `source == "library"` (a curated verified fix, not an LLM guess)
|
|
15
|
+
- `auto_safe == true` (the fix is `retry` / `refetch` / `config` — reversible, side-effect-free)
|
|
16
|
+
3. If the gate passes → applies the fix and **retries once**. If it fails → **escalates to a human** (logs the
|
|
17
|
+
verdict). It **never** auto-applies a `mutate` or `destructive` fix (create/change state, money, auth grants).
|
|
18
|
+
|
|
19
|
+
It fails **closed**: when unsure, it escalates rather than acting.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
```bash
|
|
23
|
+
# no install needed beyond the stdlib; drop the file in, or:
|
|
24
|
+
cp snapback_interceptor.py your_agent/
|
|
25
|
+
export SNAPBACK_MCP=https://api.snapback.sh/mcp # default; the free tools need no token
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Use
|
|
29
|
+
```python
|
|
30
|
+
from snapback_interceptor import SnapbackInterceptor
|
|
31
|
+
|
|
32
|
+
heal = SnapbackInterceptor(
|
|
33
|
+
auto_apply=True,
|
|
34
|
+
on_escalate=lambda v: my_ops_inbox.log(v), # human sees non-auto-safe verdicts
|
|
35
|
+
on_heal=lambda v: metrics.count("snapback.autoheal", family=v["family"]),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# wrap any call that might raise:
|
|
39
|
+
result = heal.run(call_solana_rpc, wallet, amount)
|
|
40
|
+
|
|
41
|
+
# or as a decorator:
|
|
42
|
+
@heal.guard
|
|
43
|
+
def call_solana_rpc(wallet, amount): ...
|
|
44
|
+
|
|
45
|
+
# or manually on a caught error:
|
|
46
|
+
verdict = heal.diagnose("BlockhashNotFound")
|
|
47
|
+
if verdict["gate"]["auto_apply_ok"]:
|
|
48
|
+
...apply verdict["fix"] and retry...
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## The gate contract (returned by Snapback)
|
|
52
|
+
`diagnose_infra_error` returns `{matched, family, fix, confidence, source, action_class, auto_safe, gate}`.
|
|
53
|
+
`action_class` is one of:
|
|
54
|
+
|
|
55
|
+
| class | meaning | auto-safe? |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `retry` | re-run with backoff (idempotent) | ✅ |
|
|
58
|
+
| `refetch` | re-fetch/re-price/re-sync then re-apply | ✅ |
|
|
59
|
+
| `config` | client-side param change (raise max_tokens, serve intermediate cert) | ✅ (to suggest) |
|
|
60
|
+
| `mutate` | creates/changes external state, money, auth | ❌ escalate |
|
|
61
|
+
| `destructive` | deletes/reverts/irreversible | ❌ never |
|
|
62
|
+
|
|
63
|
+
`gate.auto_apply_ok` is the ready-made verdict; the interceptor also re-derives it locally so a stale client
|
|
64
|
+
can't over-trust.
|
|
65
|
+
|
|
66
|
+
## Privacy & safety
|
|
67
|
+
- 100% client-side. Talks only to the free public `diagnose_infra_error` endpoint.
|
|
68
|
+
- Never sends your Snapback token for the diagnosis (diagnosis is free).
|
|
69
|
+
- Snapback scrubs trace content server-side; this sends only the error string you pass.
|
|
70
|
+
- Composes with `bridge.py` (post-mortem forwarding) — this is the live auto-heal layer.
|
|
71
|
+
|
|
72
|
+
## Roadmap
|
|
73
|
+
- `submit_feedback` on the retry outcome (did the fix work?) → feeds the shared library (the network effect).
|
|
74
|
+
- Hermes + LangChain adapters (same gate, different framework hook).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "snapback-selfheal"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Make your AI agent self-heal — auto-diagnose every error and apply safe fixes, gated. Snapback interceptor for OpenClaw and any Python agent."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Snapback / RA1 Labs" }]
|
|
13
|
+
keywords = ["ai-agent", "self-healing", "agentops", "openclaw", "error-recovery", "mcp", "snapback"]
|
|
14
|
+
dependencies = [] # stdlib only — no supply-chain surface
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://snapback.sh"
|
|
18
|
+
"For agents" = "https://snapback.sh/for-agents"
|
|
19
|
+
Docs = "https://snapback.sh/docs"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
# none required; the interceptor uses only urllib from the stdlib.
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
py-modules = ["snapback_interceptor"]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Snapback self-heal interceptor for OpenClaw (Sprint M / P0 — the flagship).
|
|
4
|
+
|
|
5
|
+
Turns Snapback from a tool an agent must REMEMBER to call into infrastructure that runs on EVERY error
|
|
6
|
+
automatically. Wraps your agent's tool calls; on ANY error it auto-calls diagnose_infra_error (free, <150ms,
|
|
7
|
+
no token, no LLM), applies the THREE-FACTOR GATE, and either self-heals (retry once with the fix) or escalates.
|
|
8
|
+
|
|
9
|
+
"Power tools are optional. Infrastructure is not. Make it impossible to NOT use." — validated agent feedback.
|
|
10
|
+
|
|
11
|
+
THE THREE-FACTOR GATE (auto-apply a fix without a human ONLY when ALL are true):
|
|
12
|
+
1. confidence >= 0.85 (not a low-confidence guess)
|
|
13
|
+
2. source == "library" (a curated verified fix, not an LLM inference)
|
|
14
|
+
3. auto_safe == true (action_class is retry|refetch|config — reversible / side-effect-free)
|
|
15
|
+
Snapback returns all three in the diagnose_infra_error response (+ a ready-made gate.auto_apply_ok), so this
|
|
16
|
+
interceptor just reads them. It NEVER auto-applies a 'mutate' or 'destructive' fix (create/change state, money,
|
|
17
|
+
auth grants) — those are logged + escalated to a human. Fails CLOSED: if unsure, escalate.
|
|
18
|
+
|
|
19
|
+
This is 100% CLIENT-SIDE. It talks only to the free, public diagnose_infra_error endpoint. It never touches
|
|
20
|
+
Snapback internals, never needs a paid token for the diagnosis, and never sends your secrets (Snapback also
|
|
21
|
+
scrubs server-side). Composes with bridge.py (bridge = post-mortem forwarding; this = live auto-heal).
|
|
22
|
+
|
|
23
|
+
USAGE (wrap any callable that might raise):
|
|
24
|
+
from snapback_interceptor import SnapbackInterceptor
|
|
25
|
+
heal = SnapbackInterceptor(auto_apply=True) # or auto_apply=False to only suggest/log
|
|
26
|
+
|
|
27
|
+
result = heal.run(my_tool_call, *args, **kwargs) # auto-diagnoses + retries once on a gated-safe fix
|
|
28
|
+
|
|
29
|
+
Or as a decorator:
|
|
30
|
+
@heal.guard
|
|
31
|
+
def call_solana_rpc(...): ...
|
|
32
|
+
|
|
33
|
+
Or manually on a caught error:
|
|
34
|
+
verdict = heal.diagnose("BlockhashNotFound") # {matched, family, fix, confidence, auto_safe, gate, ...}
|
|
35
|
+
if verdict["gate"]["auto_apply_ok"]:
|
|
36
|
+
...apply verdict["fix"] and retry...
|
|
37
|
+
|
|
38
|
+
ENV:
|
|
39
|
+
SNAPBACK_MCP default https://api.snapback.sh/mcp (the free tools work with no token)
|
|
40
|
+
SNAPBACK_TOKEN optional; only needed if you later wire submit_feedback on outcomes
|
|
41
|
+
SNAPBACK_CONF_MIN default 0.85 (the gate's confidence floor)
|
|
42
|
+
"""
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
import json, os, time, urllib.request, urllib.error
|
|
45
|
+
from typing import Any, Callable, Optional
|
|
46
|
+
|
|
47
|
+
_MCP = os.environ.get("SNAPBACK_MCP", "https://api.snapback.sh/mcp")
|
|
48
|
+
_TOKEN = os.environ.get("SNAPBACK_TOKEN") # optional
|
|
49
|
+
_CONF_MIN = float(os.environ.get("SNAPBACK_CONF_MIN", "0.85"))
|
|
50
|
+
_AUTO_SAFE_CLASSES = {"retry", "refetch", "config"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _mcp_call(tool: str, arguments: dict, token: Optional[str] = None, timeout: float = 8.0) -> dict:
|
|
54
|
+
"""Call a Snapback MCP tool via tools/call. diagnose_infra_error is free + needs no token."""
|
|
55
|
+
body = json.dumps({
|
|
56
|
+
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
|
57
|
+
"params": {"name": tool, "arguments": arguments},
|
|
58
|
+
}).encode()
|
|
59
|
+
headers = {"Content-Type": "application/json"}
|
|
60
|
+
if token:
|
|
61
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
62
|
+
req = urllib.request.Request(_MCP, data=body, headers=headers, method="POST")
|
|
63
|
+
try:
|
|
64
|
+
with urllib.request.urlopen(req, timeout=timeout) as r:
|
|
65
|
+
payload = json.loads(r.read().decode())
|
|
66
|
+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as e:
|
|
67
|
+
return {"matched": False, "error": f"snapback unreachable: {type(e).__name__}"}
|
|
68
|
+
# unwrap the JSON-RPC + MCP content envelope -> the tool's own JSON
|
|
69
|
+
try:
|
|
70
|
+
text = payload["result"]["content"][0]["text"]
|
|
71
|
+
return json.loads(text)
|
|
72
|
+
except (KeyError, IndexError, TypeError, json.JSONDecodeError):
|
|
73
|
+
return payload.get("result", payload)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SnapbackInterceptor:
|
|
77
|
+
"""Wrap tool calls; auto-diagnose + gate-safe self-heal on error."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, auto_apply: bool = True, conf_min: float = _CONF_MIN,
|
|
80
|
+
token: Optional[str] = _TOKEN, on_escalate: Optional[Callable[[dict], None]] = None,
|
|
81
|
+
on_heal: Optional[Callable[[dict], None]] = None):
|
|
82
|
+
"""
|
|
83
|
+
auto_apply — if True, retry once with the fix when the gate passes; if False, only diagnose + log.
|
|
84
|
+
conf_min — the gate's confidence floor (default 0.85).
|
|
85
|
+
on_escalate — callback(verdict) when a fix is NOT auto-safe (log to your ops inbox / alert a human).
|
|
86
|
+
on_heal — callback(verdict) when a fix WAS auto-applied (for telemetry).
|
|
87
|
+
"""
|
|
88
|
+
self.auto_apply = auto_apply
|
|
89
|
+
self.conf_min = conf_min
|
|
90
|
+
self.token = token
|
|
91
|
+
self.on_escalate = on_escalate or (lambda v: None)
|
|
92
|
+
self.on_heal = on_heal or (lambda v: None)
|
|
93
|
+
|
|
94
|
+
# ---- the core: diagnose one error string ----
|
|
95
|
+
def diagnose(self, error_text: str, context: str = "", action: str = "") -> dict:
|
|
96
|
+
"""Free, no-token, <150ms library match. Returns the verdict incl. the gate contract."""
|
|
97
|
+
args = {"error": str(error_text)[:4000]}
|
|
98
|
+
if context:
|
|
99
|
+
args["context"] = str(context)[:2000]
|
|
100
|
+
if action:
|
|
101
|
+
args["action"] = str(action)[:500]
|
|
102
|
+
return _mcp_call("diagnose_infra_error", args, token=None) # diagnosis is FREE — never send the token here
|
|
103
|
+
|
|
104
|
+
# ---- the three-factor gate ----
|
|
105
|
+
def gate(self, verdict: dict) -> bool:
|
|
106
|
+
"""True only when confidence>=conf_min AND source=='library' AND auto_safe. Fails CLOSED.
|
|
107
|
+
Prefer the server's ready-made gate.auto_apply_ok; re-check locally so a stale client can't over-trust."""
|
|
108
|
+
if not verdict or not verdict.get("matched"):
|
|
109
|
+
return False
|
|
110
|
+
conf = float(verdict.get("confidence") or 0.0)
|
|
111
|
+
source = verdict.get("source")
|
|
112
|
+
action_class = verdict.get("action_class")
|
|
113
|
+
auto_safe = verdict.get("auto_safe")
|
|
114
|
+
# local re-derivation of the contract (don't blindly trust a single server field)
|
|
115
|
+
safe = bool(auto_safe) if auto_safe is not None else (action_class in _AUTO_SAFE_CLASSES)
|
|
116
|
+
local_ok = (conf >= self.conf_min) and (source == "library") and safe
|
|
117
|
+
server_ok = bool((verdict.get("gate") or {}).get("auto_apply_ok"))
|
|
118
|
+
return local_ok and (server_ok or verdict.get("gate") is None)
|
|
119
|
+
|
|
120
|
+
# ---- report a gated auto-apply outcome (safety telemetry + feeds the moat). Best-effort, needs a token. ----
|
|
121
|
+
def _report_outcome(self, verdict: dict, succeeded: bool) -> None:
|
|
122
|
+
if not self.token:
|
|
123
|
+
return # report_outcome is token-scoped (attributes to your org); skip silently if no token
|
|
124
|
+
try:
|
|
125
|
+
_mcp_call("report_outcome", {
|
|
126
|
+
"failure_class": verdict.get("failure_class") or verdict.get("family"),
|
|
127
|
+
"family": verdict.get("family"),
|
|
128
|
+
"fix": verdict.get("fix"),
|
|
129
|
+
"confidence": verdict.get("confidence"),
|
|
130
|
+
"action_class": verdict.get("action_class"),
|
|
131
|
+
"succeeded": bool(succeeded),
|
|
132
|
+
}, token=self.token)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass # telemetry must never break the caller
|
|
135
|
+
|
|
136
|
+
# ---- wrap a call: auto-diagnose + optionally self-heal ----
|
|
137
|
+
def run(self, fn: Callable, *args, _fix_kwarg: Optional[str] = None, **kwargs) -> Any:
|
|
138
|
+
"""Run fn; on error, diagnose. If the gate passes AND auto_apply, retry ONCE. Else re-raise after logging.
|
|
139
|
+
_fix_kwarg: if your fn accepts a hint kwarg (e.g. retry_after / create_ata=True), name it and the fix
|
|
140
|
+
text is passed through; otherwise the retry is a plain re-run (correct for transient retry/refetch)."""
|
|
141
|
+
try:
|
|
142
|
+
return fn(*args, **kwargs)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
verdict = self.diagnose(f"{type(e).__name__}: {e}")
|
|
145
|
+
verdict["_original_error"] = f"{type(e).__name__}: {e}"
|
|
146
|
+
if self.gate(verdict):
|
|
147
|
+
self.on_heal(verdict)
|
|
148
|
+
if self.auto_apply:
|
|
149
|
+
# gated-safe fixes are retry/refetch/config — a single re-run is the correct action for
|
|
150
|
+
# transient (retry) + re-fetch-then-reapply (refetch). For config the fix is advisory.
|
|
151
|
+
if _fix_kwarg:
|
|
152
|
+
kwargs[_fix_kwarg] = verdict.get("fix")
|
|
153
|
+
time.sleep(0.5) # brief backoff before the single retry
|
|
154
|
+
try:
|
|
155
|
+
result = fn(*args, **kwargs)
|
|
156
|
+
self._report_outcome(verdict, succeeded=True) # feeds safety telemetry + the moat
|
|
157
|
+
return result
|
|
158
|
+
except Exception:
|
|
159
|
+
# retry didn't fix it -> report the miss + escalate with the verdict for a human
|
|
160
|
+
self._report_outcome(verdict, succeeded=False)
|
|
161
|
+
self.on_escalate(verdict)
|
|
162
|
+
raise
|
|
163
|
+
# auto_apply off: healed-verdict available but we don't act
|
|
164
|
+
raise
|
|
165
|
+
else:
|
|
166
|
+
# NOT gated-safe (low conf, LLM source, or a mutate/destructive fix) -> escalate, never auto-act
|
|
167
|
+
self.on_escalate(verdict)
|
|
168
|
+
raise
|
|
169
|
+
|
|
170
|
+
def guard(self, fn: Callable) -> Callable:
|
|
171
|
+
"""Decorator form of run()."""
|
|
172
|
+
def wrapped(*args, **kwargs):
|
|
173
|
+
return self.run(fn, *args, **kwargs)
|
|
174
|
+
wrapped.__name__ = getattr(fn, "__name__", "guarded")
|
|
175
|
+
return wrapped
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---- tiny demo / smoke test ----
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
heal = SnapbackInterceptor(
|
|
181
|
+
auto_apply=False, # demo: diagnose + print, don't retry
|
|
182
|
+
on_escalate=lambda v: print(f"[ESCALATE] {v.get('family')} · conf={v.get('confidence')} · "
|
|
183
|
+
f"action_class={v.get('action_class')} · reason={(v.get('gate') or {}).get('reason')}"),
|
|
184
|
+
on_heal=lambda v: print(f"[AUTO-HEAL OK] {v.get('family')} · {v.get('fix')}"),
|
|
185
|
+
)
|
|
186
|
+
for err in ["deadlock detected 40P01",
|
|
187
|
+
"unable to get local issuer certificate",
|
|
188
|
+
"outcome type blocked do_not_try_again", # mutate -> must escalate, never auto-heal
|
|
189
|
+
"invalid_grant refresh token"]: # mutate -> escalate
|
|
190
|
+
v = heal.diagnose(err)
|
|
191
|
+
gated = heal.gate(v)
|
|
192
|
+
print(f"\n{err!r}\n family={v.get('family')} conf={v.get('confidence')} "
|
|
193
|
+
f"source={v.get('source')} action_class={v.get('action_class')} "
|
|
194
|
+
f"auto_safe={v.get('auto_safe')} -> GATE {'PASS (self-heal)' if gated else 'FAIL (escalate)'}")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snapback-selfheal
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Make your AI agent self-heal — auto-diagnose every error and apply safe fixes, gated. Snapback interceptor for OpenClaw and any Python agent.
|
|
5
|
+
Author: Snapback / RA1 Labs
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://snapback.sh
|
|
8
|
+
Project-URL: For agents, https://snapback.sh/for-agents
|
|
9
|
+
Project-URL: Docs, https://snapback.sh/docs
|
|
10
|
+
Keywords: ai-agent,self-healing,agentops,openclaw,error-recovery,mcp,snapback
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Snapback self-heal interceptor for OpenClaw
|
|
15
|
+
|
|
16
|
+
**Make Snapback automatic.** Instead of remembering to call the diagnosis tool, wrap your agent's tool calls
|
|
17
|
+
once — and every error is auto-diagnosed, and gated-safe fixes are applied and retried *without a human*.
|
|
18
|
+
|
|
19
|
+
> "Power tools are optional. Infrastructure is not." This turns Snapback from a tool you reach for into
|
|
20
|
+
> infrastructure that runs on every failure.
|
|
21
|
+
|
|
22
|
+
## What it does
|
|
23
|
+
On **any** tool-call error, the interceptor:
|
|
24
|
+
1. Calls `diagnose_infra_error` (free, no token, no LLM, <150ms).
|
|
25
|
+
2. Applies the **three-factor gate** — auto-apply a fix only when **all** are true:
|
|
26
|
+
- `confidence >= 0.85`
|
|
27
|
+
- `source == "library"` (a curated verified fix, not an LLM guess)
|
|
28
|
+
- `auto_safe == true` (the fix is `retry` / `refetch` / `config` — reversible, side-effect-free)
|
|
29
|
+
3. If the gate passes → applies the fix and **retries once**. If it fails → **escalates to a human** (logs the
|
|
30
|
+
verdict). It **never** auto-applies a `mutate` or `destructive` fix (create/change state, money, auth grants).
|
|
31
|
+
|
|
32
|
+
It fails **closed**: when unsure, it escalates rather than acting.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
```bash
|
|
36
|
+
# no install needed beyond the stdlib; drop the file in, or:
|
|
37
|
+
cp snapback_interceptor.py your_agent/
|
|
38
|
+
export SNAPBACK_MCP=https://api.snapback.sh/mcp # default; the free tools need no token
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use
|
|
42
|
+
```python
|
|
43
|
+
from snapback_interceptor import SnapbackInterceptor
|
|
44
|
+
|
|
45
|
+
heal = SnapbackInterceptor(
|
|
46
|
+
auto_apply=True,
|
|
47
|
+
on_escalate=lambda v: my_ops_inbox.log(v), # human sees non-auto-safe verdicts
|
|
48
|
+
on_heal=lambda v: metrics.count("snapback.autoheal", family=v["family"]),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# wrap any call that might raise:
|
|
52
|
+
result = heal.run(call_solana_rpc, wallet, amount)
|
|
53
|
+
|
|
54
|
+
# or as a decorator:
|
|
55
|
+
@heal.guard
|
|
56
|
+
def call_solana_rpc(wallet, amount): ...
|
|
57
|
+
|
|
58
|
+
# or manually on a caught error:
|
|
59
|
+
verdict = heal.diagnose("BlockhashNotFound")
|
|
60
|
+
if verdict["gate"]["auto_apply_ok"]:
|
|
61
|
+
...apply verdict["fix"] and retry...
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## The gate contract (returned by Snapback)
|
|
65
|
+
`diagnose_infra_error` returns `{matched, family, fix, confidence, source, action_class, auto_safe, gate}`.
|
|
66
|
+
`action_class` is one of:
|
|
67
|
+
|
|
68
|
+
| class | meaning | auto-safe? |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `retry` | re-run with backoff (idempotent) | ✅ |
|
|
71
|
+
| `refetch` | re-fetch/re-price/re-sync then re-apply | ✅ |
|
|
72
|
+
| `config` | client-side param change (raise max_tokens, serve intermediate cert) | ✅ (to suggest) |
|
|
73
|
+
| `mutate` | creates/changes external state, money, auth | ❌ escalate |
|
|
74
|
+
| `destructive` | deletes/reverts/irreversible | ❌ never |
|
|
75
|
+
|
|
76
|
+
`gate.auto_apply_ok` is the ready-made verdict; the interceptor also re-derives it locally so a stale client
|
|
77
|
+
can't over-trust.
|
|
78
|
+
|
|
79
|
+
## Privacy & safety
|
|
80
|
+
- 100% client-side. Talks only to the free public `diagnose_infra_error` endpoint.
|
|
81
|
+
- Never sends your Snapback token for the diagnosis (diagnosis is free).
|
|
82
|
+
- Snapback scrubs trace content server-side; this sends only the error string you pass.
|
|
83
|
+
- Composes with `bridge.py` (post-mortem forwarding) — this is the live auto-heal layer.
|
|
84
|
+
|
|
85
|
+
## Roadmap
|
|
86
|
+
- `submit_feedback` on the retry outcome (did the fix work?) → feeds the shared library (the network effect).
|
|
87
|
+
- Hermes + LangChain adapters (same gate, different framework hook).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
snapback_interceptor
|