safety-protocol 0.3.0__py3-none-any.whl
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.
- safety_protocol/__init__.py +201 -0
- safety_protocol/cli.py +132 -0
- safety_protocol/core.py +424 -0
- safety_protocol/data/guard_config.json +59 -0
- safety_protocol/deployment.py +384 -0
- safety_protocol/guard_service.py +241 -0
- safety_protocol/insurance.py +221 -0
- safety_protocol/onchain.py +271 -0
- safety_protocol/onchain_audit.py +346 -0
- safety_protocol/onchain_payment_verifier.py +174 -0
- safety_protocol/payments.py +380 -0
- safety_protocol/protocol.py +547 -0
- safety_protocol/real_wallet.py +189 -0
- safety_protocol/scope_linter.py +191 -0
- safety_protocol-0.3.0.dist-info/METADATA +217 -0
- safety_protocol-0.3.0.dist-info/RECORD +20 -0
- safety_protocol-0.3.0.dist-info/WHEEL +5 -0
- safety_protocol-0.3.0.dist-info/entry_points.txt +2 -0
- safety_protocol-0.3.0.dist-info/licenses/LICENSE +21 -0
- safety_protocol-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Safety Protocol Framework for LLM Agents
|
|
3
|
+
|
|
4
|
+
A framework for building safe, accountable AI agents that operate
|
|
5
|
+
through enforced safety protocols rather than relying on the model's
|
|
6
|
+
good behavior.
|
|
7
|
+
|
|
8
|
+
The core principle: the user is accountable for their agents. Every
|
|
9
|
+
agent action passes through a safety protocol that enforces binding,
|
|
10
|
+
scope, budget, approval gates, monitoring, audit trail, and kill switch.
|
|
11
|
+
|
|
12
|
+
The agent operates freely WITHIN these bounds. The bounds are enforced
|
|
13
|
+
by infrastructure, not by hoping the agent behaves.
|
|
14
|
+
|
|
15
|
+
When the agent messes up, the audit trail tells you what happened, the
|
|
16
|
+
binding tells you who's accountable, and the next iteration has tighter
|
|
17
|
+
scope.
|
|
18
|
+
|
|
19
|
+
This framework implements a reference architecture for safe agents:
|
|
20
|
+
|
|
21
|
+
Layer 1: Safety Protocol (enforcement) — scope, budget, approval, monitoring,
|
|
22
|
+
audit trail (off-chain complete record), kill switch.
|
|
23
|
+
|
|
24
|
+
Layer 2: On-chain anchors — SBT-style non-transferable binding, on-chain audit
|
|
25
|
+
for key events (high-value actions, approvals, kill switch, scope violations).
|
|
26
|
+
Verifiable by anyone. Tamper-resistant.
|
|
27
|
+
|
|
28
|
+
Layer 3: Insurance interface — claims-ready evidence, underwriter reports,
|
|
29
|
+
control-adjusted exposure estimates. The protocol makes the agent insurable
|
|
30
|
+
by providing verifiable evidence of what happened and controls that operated.
|
|
31
|
+
|
|
32
|
+
The framework is framework-agnostic: works with any LLM, any runtime, any chain.
|
|
33
|
+
In production, swap the simulated on-chain layer for real ERC-5192/ERC-8004 + a
|
|
34
|
+
real chain, and the insurance interface for a real insurer's claims process.
|
|
35
|
+
|
|
36
|
+
Installation:
|
|
37
|
+
pip install safety-protocol
|
|
38
|
+
|
|
39
|
+
Quick start:
|
|
40
|
+
|
|
41
|
+
from safety_protocol import (
|
|
42
|
+
SafetyProtocol, BoundAgent, ScopeRule, AuditTrail, Monitor
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
audit = AuditTrail()
|
|
46
|
+
protocol = SafetyProtocol(
|
|
47
|
+
agent_id="agent-001",
|
|
48
|
+
user_id="alice",
|
|
49
|
+
scope_rules=[
|
|
50
|
+
ScopeRule(
|
|
51
|
+
action_type="api_call",
|
|
52
|
+
allowed_targets=["https://api.example.com/v1/*"],
|
|
53
|
+
forbidden_targets=["admin", "billing"],
|
|
54
|
+
max_cost=5.0,
|
|
55
|
+
),
|
|
56
|
+
],
|
|
57
|
+
budget_limit=50.0,
|
|
58
|
+
approval_threshold_cost=10.0,
|
|
59
|
+
audit=audit,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
agent = BoundAgent(
|
|
63
|
+
agent_id="agent-001",
|
|
64
|
+
user_id="alice",
|
|
65
|
+
safety_protocol=protocol,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
result = agent.propose_action(
|
|
69
|
+
action_type="api_call",
|
|
70
|
+
target="https://api.example.com/v1/search",
|
|
71
|
+
params={"query": "test"},
|
|
72
|
+
estimated_cost=2.50,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
print(result.outcome) # ActionOutcome.ALLOWED
|
|
76
|
+
|
|
77
|
+
For the full reference architecture with on-chain binding, on-chain audit,
|
|
78
|
+
and insurance interface:
|
|
79
|
+
|
|
80
|
+
from safety_protocol.deployment import ReferenceDeployment
|
|
81
|
+
|
|
82
|
+
deployment = ReferenceDeployment(
|
|
83
|
+
agent_id="agent-001",
|
|
84
|
+
user_id="alice",
|
|
85
|
+
agent_name="ResearchAgent",
|
|
86
|
+
agent_role="AI research assistant",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Agent proposes action — full stack enforces
|
|
90
|
+
result = deployment.agent_propose_action(
|
|
91
|
+
action_type="api_call",
|
|
92
|
+
target="https://api.example.com/v1/search",
|
|
93
|
+
params={"query": "test"},
|
|
94
|
+
estimated_cost=2.50,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Get binding proof (verifiable by anyone)
|
|
98
|
+
proof = deployment.get_binding_proof()
|
|
99
|
+
|
|
100
|
+
# Get claims evidence (for insurance)
|
|
101
|
+
evidence = deployment.get_claim_evidence(
|
|
102
|
+
claim_description="Agent made incorrect API call",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Get underwriter package (for binding coverage)
|
|
106
|
+
underwriter = deployment.get_underwriter_package(
|
|
107
|
+
agent_description="Research agent that calls APIs",
|
|
108
|
+
task_profile="API calls, message sending",
|
|
109
|
+
max_potential_loss=5000.0,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
The problem this solves:
|
|
113
|
+
-----------------------
|
|
114
|
+
AI agents can take actions with real consequences: spending money, calling
|
|
115
|
+
APIs, modifying systems, sending messages, spawning subagents. The model's
|
|
116
|
+
good behavior is not reliable enough to depend on. The safety protocol makes
|
|
117
|
+
the agent *safe to operate* by enforcing constraints in infrastructure, not
|
|
118
|
+
in prompts.
|
|
119
|
+
|
|
120
|
+
When the agent messes up, you need to know: what happened, who's accountable,
|
|
121
|
+
what controls operated, what can be reconstructed. The binding + audit trail
|
|
122
|
+
+ on-chain record answer these questions. The insurance interface makes the
|
|
123
|
+
agent insurable by providing claims-ready evidence.
|
|
124
|
+
|
|
125
|
+
The enforcement library (Layer 1) is production-usable. The on-chain layer is
|
|
126
|
+
simulated (same interface as a real chain). In production, use ERC-5192 SBT or
|
|
127
|
+
ERC-8004 on a real chain, and connect the insurance interface to a real
|
|
128
|
+
insurer's claims process.
|
|
129
|
+
|
|
130
|
+
The architecture is composable: use the protocol alone, add on-chain binding,
|
|
131
|
+
add on-chain audit, add insurance interface — each layer is independent and
|
|
132
|
+
adds value on its own.
|
|
133
|
+
|
|
134
|
+
Documentation:
|
|
135
|
+
- Core protocol: src/safety_protocol/protocol.py
|
|
136
|
+
- On-chain binding: src/safety_protocol/onchain.py
|
|
137
|
+
- On-chain audit: src/safety_protocol/onchain_audit.py
|
|
138
|
+
- Insurance interface: src/safety_protocol/insurance.py
|
|
139
|
+
- Reference deployment: src/safety_protocol/deployment.py
|
|
140
|
+
- Example (LLM agent): examples/llm_agent.py
|
|
141
|
+
- Example (reference deployment): examples/reference_deployment.py
|
|
142
|
+
"""
|
|
143
|
+
from .core import (
|
|
144
|
+
ActionOutcome,
|
|
145
|
+
ActionRequest,
|
|
146
|
+
ActionResult,
|
|
147
|
+
ApprovalRecord,
|
|
148
|
+
AuditTrail,
|
|
149
|
+
Monitor,
|
|
150
|
+
ProtocolState,
|
|
151
|
+
ScopeRule,
|
|
152
|
+
)
|
|
153
|
+
from .deployment import ReferenceDeployment
|
|
154
|
+
from .guard_service import GuardService, build_protocol_from_config
|
|
155
|
+
from .insurance import InsuranceInterface
|
|
156
|
+
from .onchain import OnChainBindingRegistry, OnChainBoundProtocol
|
|
157
|
+
from .onchain_audit import DualAudit, OnChainAudit
|
|
158
|
+
from .onchain_payment_verifier import OnChainPaymentVerifier
|
|
159
|
+
from .payments import PaymentEnvelope, SafeSpendAgent, SimWallet
|
|
160
|
+
from .protocol import BoundAgent, SafetyProtocol
|
|
161
|
+
from .real_wallet import HAS_REAL_CRYPTO, RealWallet
|
|
162
|
+
from .scope_linter import Finding, Severity, lint_report, lint_rules
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
from importlib.metadata import PackageNotFoundError
|
|
166
|
+
from importlib.metadata import version as _version
|
|
167
|
+
|
|
168
|
+
__version__ = _version("safety-protocol")
|
|
169
|
+
except PackageNotFoundError: # running from a source checkout
|
|
170
|
+
__version__ = "0.3.0"
|
|
171
|
+
__all__ = [
|
|
172
|
+
"ActionOutcome",
|
|
173
|
+
"ProtocolState",
|
|
174
|
+
"ActionRequest",
|
|
175
|
+
"ActionResult",
|
|
176
|
+
"ApprovalRecord",
|
|
177
|
+
"ScopeRule",
|
|
178
|
+
"AuditTrail",
|
|
179
|
+
"Monitor",
|
|
180
|
+
"SafetyProtocol",
|
|
181
|
+
"BoundAgent",
|
|
182
|
+
"OnChainBindingRegistry",
|
|
183
|
+
"OnChainBoundProtocol",
|
|
184
|
+
"OnChainAudit",
|
|
185
|
+
"DualAudit",
|
|
186
|
+
"InsuranceInterface",
|
|
187
|
+
"ReferenceDeployment",
|
|
188
|
+
"SafeSpendAgent",
|
|
189
|
+
"SimWallet",
|
|
190
|
+
"PaymentEnvelope",
|
|
191
|
+
"RealWallet",
|
|
192
|
+
"HAS_REAL_CRYPTO",
|
|
193
|
+
"OnChainPaymentVerifier",
|
|
194
|
+
"lint_rules",
|
|
195
|
+
"lint_report",
|
|
196
|
+
"Finding",
|
|
197
|
+
"Severity",
|
|
198
|
+
"GuardService",
|
|
199
|
+
"build_protocol_from_config",
|
|
200
|
+
"__version__",
|
|
201
|
+
]
|
safety_protocol/cli.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
``safety-guard`` — the CLI developers use to run and lint the guard.
|
|
4
|
+
|
|
5
|
+
Subcommands (all read the USER-CONTROLLED config; none let the agent widen
|
|
6
|
+
scope):
|
|
7
|
+
|
|
8
|
+
init write a default, least-privilege config to disk
|
|
9
|
+
serve start the HTTP guard (fails closed if rules fail lint)
|
|
10
|
+
check ask the guard whether an intent is allowed (--action/--target/...)
|
|
11
|
+
pay request a gated payment (--recipient/--usd)
|
|
12
|
+
lint lint the config ruleset and exit non-zero if too broad
|
|
13
|
+
|
|
14
|
+
Run:
|
|
15
|
+
safety-guard init --out guard_config.json
|
|
16
|
+
safety-guard serve --config guard_config.json
|
|
17
|
+
safety-guard check --config guard_config.json \\
|
|
18
|
+
--action api_call --target https://api.research.example/v1/search \\
|
|
19
|
+
--method POST --params '{"query":"hi"}'
|
|
20
|
+
safety-guard pay --config guard_config.json \\
|
|
21
|
+
--recipient 0xMerchant1111111111111111111111111111111 --usd 0.10
|
|
22
|
+
safety-guard lint --config guard_config.json
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import sys
|
|
29
|
+
from importlib.resources import files
|
|
30
|
+
|
|
31
|
+
from safety_protocol.guard_service import GuardService
|
|
32
|
+
from safety_protocol.guard_service import serve as serve_http
|
|
33
|
+
from safety_protocol.scope_linter import Severity
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_cfg(path):
|
|
37
|
+
with open(path) as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def cmd_init(args):
|
|
42
|
+
"""Write the packaged default (least-privilege) config to disk."""
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
if os.path.exists(args.out) and not args.force:
|
|
46
|
+
print(f"refusing to overwrite existing file: {args.out} (use --force)")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
src = files("safety_protocol").joinpath("data/guard_config.json")
|
|
49
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
50
|
+
f.write(src.read_text(encoding="utf-8"))
|
|
51
|
+
print(f"wrote default guard config to {args.out}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_serve(args):
|
|
55
|
+
# Delegate to the HTTP server entry (fails closed on lint via GuardService).
|
|
56
|
+
serve_http(args.config, args.host, args.port)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def cmd_check(args):
|
|
60
|
+
svc = GuardService(load_cfg(args.config))
|
|
61
|
+
params = json.loads(args.params) if args.params else {}
|
|
62
|
+
r = svc.guard(args.action, args.target, method=args.method,
|
|
63
|
+
params=params, cost=args.cost)
|
|
64
|
+
print(json.dumps(r, indent=2))
|
|
65
|
+
sys.exit(0 if r["allowed"] else 1)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cmd_pay(args):
|
|
69
|
+
svc = GuardService(load_cfg(args.config))
|
|
70
|
+
r = svc.pay(args.recipient, args.usd, resource=args.resource)
|
|
71
|
+
print(json.dumps(r, indent=2))
|
|
72
|
+
sys.exit(0 if r["allowed"] else 1)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cmd_lint(args):
|
|
76
|
+
cfg = load_cfg(args.config)
|
|
77
|
+
# Rebuild via the same path the guard uses, so lint + fail-closed match.
|
|
78
|
+
from safety_protocol.guard_service import build_protocol_from_config
|
|
79
|
+
from safety_protocol.scope_linter import lint_report
|
|
80
|
+
try:
|
|
81
|
+
_, findings = build_protocol_from_config(cfg)
|
|
82
|
+
except SystemExit as e:
|
|
83
|
+
# fail-closed: broad rules -> non-zero exit
|
|
84
|
+
print(str(e))
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
print(lint_report(findings))
|
|
87
|
+
blocking = [f for f in findings
|
|
88
|
+
if f.severity in (Severity.ERROR, Severity.WARN)]
|
|
89
|
+
sys.exit(1 if blocking else 0)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main(argv=None):
|
|
93
|
+
ap = argparse.ArgumentParser(prog="safety-guard", description="Safety guard CLI")
|
|
94
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
95
|
+
|
|
96
|
+
i = sub.add_parser("init", help="write a default guard config")
|
|
97
|
+
i.add_argument("--out", default="guard_config.json")
|
|
98
|
+
i.add_argument("--force", action="store_true", help="overwrite if it exists")
|
|
99
|
+
i.set_defaults(func=cmd_init)
|
|
100
|
+
|
|
101
|
+
s = sub.add_parser("serve", help="start the HTTP guard")
|
|
102
|
+
s.add_argument("--config", required=True)
|
|
103
|
+
s.add_argument("--host", default="127.0.0.1")
|
|
104
|
+
s.add_argument("--port", type=int, default=8080)
|
|
105
|
+
s.set_defaults(func=cmd_serve)
|
|
106
|
+
|
|
107
|
+
c = sub.add_parser("check", help="check if an intent is allowed")
|
|
108
|
+
c.add_argument("--config", required=True)
|
|
109
|
+
c.add_argument("--action", required=True)
|
|
110
|
+
c.add_argument("--target", required=True)
|
|
111
|
+
c.add_argument("--method")
|
|
112
|
+
c.add_argument("--params", default="")
|
|
113
|
+
c.add_argument("--cost", type=float, default=0.0)
|
|
114
|
+
c.set_defaults(func=cmd_check)
|
|
115
|
+
|
|
116
|
+
p = sub.add_parser("pay", help="request a gated payment")
|
|
117
|
+
p.add_argument("--config", required=True)
|
|
118
|
+
p.add_argument("--recipient", required=True)
|
|
119
|
+
p.add_argument("--usd", type=float, required=True)
|
|
120
|
+
p.add_argument("--resource", default="weather")
|
|
121
|
+
p.set_defaults(func=cmd_pay)
|
|
122
|
+
|
|
123
|
+
lint_p = sub.add_parser("lint", help="lint the config ruleset")
|
|
124
|
+
lint_p.add_argument("--config", required=True)
|
|
125
|
+
lint_p.set_defaults(func=cmd_lint)
|
|
126
|
+
|
|
127
|
+
args = ap.parse_args(argv)
|
|
128
|
+
args.func(args)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|