@agentlayer.tech/wallet 0.1.48 → 0.1.50
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.
- package/.openclaw/extensions/agent-wallet/index.ts +45 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/autonomous_permissions.py +145 -0
- package/agent-wallet/agent_wallet/autonomous_policy.py +305 -0
- package/agent-wallet/agent_wallet/autonomous_session.py +236 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +507 -29
- package/agent-wallet/agent_wallet/openclaw_cli.py +36 -0
- package/agent-wallet/agent_wallet/spending_limits.py +21 -3
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +2 -2
- package/bin/openclaw-agent-wallet.mjs +298 -7
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/README.md +5 -0
- package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-approve.md +24 -0
- package/claude-code/plugins/agent-wallet/commands/agentlayer-autonomous-revoke.md +16 -0
- package/claude-code/plugins/agent-wallet/scripts/bootstrap_backend.sh +2 -2
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/server.py +18 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/package.json +1 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Persistent autonomous-session store.
|
|
2
|
+
|
|
3
|
+
The OpenClaw wallet CLI is invoked as a fresh subprocess per tool call, so an
|
|
4
|
+
autonomous session (the "envelope" a human authorizes once, inside which the
|
|
5
|
+
agent may execute without per-transaction confirmation) cannot live in memory.
|
|
6
|
+
This module persists the session — its :class:`AutonomousSessionConfig`, start
|
|
7
|
+
time, operation count, and spend ledger — to a JSON file under
|
|
8
|
+
``OPENCLAW_HOME`` so the same limits are enforced across every subprocess.
|
|
9
|
+
|
|
10
|
+
Trust model:
|
|
11
|
+
|
|
12
|
+
* ``start_session`` writes the envelope. It is expected to be called only after
|
|
13
|
+
a human authorizes the exact limits (the adapter gates the agent-facing
|
|
14
|
+
``start_autonomous_session`` tool behind a host-issued approval token, so the
|
|
15
|
+
agent cannot widen its own permissions).
|
|
16
|
+
* ``stop_session`` removes the envelope and is always safe to call (it can only
|
|
17
|
+
*reduce* what the agent may do).
|
|
18
|
+
* ``authorize_operation`` rehydrates the engine from the persisted record,
|
|
19
|
+
evaluates the operation, persists the updated counters/spend, and returns a
|
|
20
|
+
signed approval token — or raises :class:`WalletBackendError` on denial.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from agent_wallet.autonomous_policy import (
|
|
33
|
+
AutonomousPolicyEngine,
|
|
34
|
+
AutonomousSessionConfig,
|
|
35
|
+
OperationRequest,
|
|
36
|
+
)
|
|
37
|
+
from agent_wallet.config import resolve_openclaw_home
|
|
38
|
+
from agent_wallet.spending_limits import SpendingConfig, SpendingLedger
|
|
39
|
+
from agent_wallet.wallet_layer.base import WalletBackendError
|
|
40
|
+
|
|
41
|
+
SESSION_VERSION = 1
|
|
42
|
+
_SESSION_FILENAME = "autonomous_session.json"
|
|
43
|
+
_lock = threading.Lock()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _session_path() -> Path:
|
|
47
|
+
return resolve_openclaw_home() / _SESSION_FILENAME
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Config (de)serialization
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def config_to_dict(config: AutonomousSessionConfig) -> dict[str, Any]:
|
|
55
|
+
cfg = config.normalized()
|
|
56
|
+
return {
|
|
57
|
+
"enabled": cfg.enabled,
|
|
58
|
+
"allowed_tools": sorted(cfg.allowed_tools),
|
|
59
|
+
"allowed_networks": sorted(cfg.allowed_networks),
|
|
60
|
+
"allow_mainnet": cfg.allow_mainnet,
|
|
61
|
+
"allowed_recipients": sorted(cfg.allowed_recipients),
|
|
62
|
+
"allow_any_recipient": cfg.allow_any_recipient,
|
|
63
|
+
"require_simulation": cfg.require_simulation,
|
|
64
|
+
"spending": {
|
|
65
|
+
"max_per_tx_lamports": cfg.spending.max_per_tx_lamports,
|
|
66
|
+
"max_hourly_lamports": cfg.spending.max_hourly_lamports,
|
|
67
|
+
"max_daily_lamports": cfg.spending.max_daily_lamports,
|
|
68
|
+
"max_txs_per_minute": cfg.spending.max_txs_per_minute,
|
|
69
|
+
},
|
|
70
|
+
"max_operations": cfg.max_operations,
|
|
71
|
+
"session_ttl_seconds": cfg.session_ttl_seconds,
|
|
72
|
+
"approval_ttl_seconds": cfg.approval_ttl_seconds,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def config_from_dict(data: dict[str, Any]) -> AutonomousSessionConfig:
|
|
77
|
+
spending = data.get("spending") or {}
|
|
78
|
+
return AutonomousSessionConfig(
|
|
79
|
+
enabled=bool(data.get("enabled", False)),
|
|
80
|
+
allowed_tools=frozenset(data.get("allowed_tools") or []),
|
|
81
|
+
allowed_networks=frozenset(data.get("allowed_networks") or []),
|
|
82
|
+
allow_mainnet=bool(data.get("allow_mainnet", False)),
|
|
83
|
+
allowed_recipients=frozenset(data.get("allowed_recipients") or []),
|
|
84
|
+
allow_any_recipient=bool(data.get("allow_any_recipient", False)),
|
|
85
|
+
require_simulation=bool(data.get("require_simulation", True)),
|
|
86
|
+
spending=SpendingConfig(
|
|
87
|
+
max_per_tx_lamports=int(spending.get("max_per_tx_lamports", 0) or 0),
|
|
88
|
+
max_hourly_lamports=int(spending.get("max_hourly_lamports", 0) or 0),
|
|
89
|
+
max_daily_lamports=int(spending.get("max_daily_lamports", 0) or 0),
|
|
90
|
+
max_txs_per_minute=int(spending.get("max_txs_per_minute", 0) or 0),
|
|
91
|
+
),
|
|
92
|
+
max_operations=int(data.get("max_operations", 0) or 0),
|
|
93
|
+
session_ttl_seconds=int(data.get("session_ttl_seconds", 0) or 0),
|
|
94
|
+
approval_ttl_seconds=int(data.get("approval_ttl_seconds", 120) or 120),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Record persistence
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _load_record() -> dict[str, Any] | None:
|
|
103
|
+
path = _session_path()
|
|
104
|
+
if not path.exists():
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
record = json.loads(path.read_text(encoding="utf-8"))
|
|
108
|
+
except (OSError, ValueError):
|
|
109
|
+
return None
|
|
110
|
+
if not isinstance(record, dict) or int(record.get("version") or 0) != SESSION_VERSION:
|
|
111
|
+
return None
|
|
112
|
+
return record
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _write_record(record: dict[str, Any]) -> None:
|
|
116
|
+
path = _session_path()
|
|
117
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
tmp = path.with_suffix(".json.tmp")
|
|
119
|
+
tmp.write_text(json.dumps(record, separators=(",", ":")), encoding="utf-8")
|
|
120
|
+
os.replace(tmp, path) # atomic on POSIX
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Public API
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def start_session(config: AutonomousSessionConfig) -> dict[str, Any]:
|
|
128
|
+
"""Persist a new autonomous session envelope and return its status."""
|
|
129
|
+
if not config.enabled:
|
|
130
|
+
raise WalletBackendError("Cannot start an autonomous session with enabled=false.")
|
|
131
|
+
record = {
|
|
132
|
+
"version": SESSION_VERSION,
|
|
133
|
+
"started_at": time.time(),
|
|
134
|
+
"operations": 0,
|
|
135
|
+
"spend_entries": [],
|
|
136
|
+
"config": config_to_dict(config),
|
|
137
|
+
}
|
|
138
|
+
with _lock:
|
|
139
|
+
_write_record(record)
|
|
140
|
+
return session_status()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def stop_session() -> dict[str, Any]:
|
|
144
|
+
"""Remove any active session. Always safe (only reduces permissions)."""
|
|
145
|
+
with _lock:
|
|
146
|
+
path = _session_path()
|
|
147
|
+
existed = path.exists()
|
|
148
|
+
try:
|
|
149
|
+
path.unlink()
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
existed = False
|
|
152
|
+
return {"active": False, "stopped": existed}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_active() -> bool:
|
|
156
|
+
record = _load_record()
|
|
157
|
+
return bool(record and (record.get("config") or {}).get("enabled"))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def session_status() -> dict[str, Any]:
|
|
161
|
+
"""Return a non-secret view of the current session for the agent/host."""
|
|
162
|
+
record = _load_record()
|
|
163
|
+
if not record:
|
|
164
|
+
return {"active": False}
|
|
165
|
+
cfg = record.get("config") or {}
|
|
166
|
+
started_at = float(record.get("started_at") or 0.0)
|
|
167
|
+
ttl = int(cfg.get("session_ttl_seconds", 0) or 0)
|
|
168
|
+
status: dict[str, Any] = {
|
|
169
|
+
"active": bool(cfg.get("enabled")),
|
|
170
|
+
"started_at": started_at,
|
|
171
|
+
"operations": int(record.get("operations") or 0),
|
|
172
|
+
"max_operations": int(cfg.get("max_operations", 0) or 0),
|
|
173
|
+
"allow_mainnet": bool(cfg.get("allow_mainnet", False)),
|
|
174
|
+
"allowed_tools": cfg.get("allowed_tools") or [],
|
|
175
|
+
"allowed_networks": cfg.get("allowed_networks") or [],
|
|
176
|
+
"allow_any_recipient": bool(cfg.get("allow_any_recipient", False)),
|
|
177
|
+
"require_simulation": bool(cfg.get("require_simulation", True)),
|
|
178
|
+
"spending": cfg.get("spending") or {},
|
|
179
|
+
}
|
|
180
|
+
if ttl > 0:
|
|
181
|
+
status["expires_at"] = started_at + ttl
|
|
182
|
+
status["expired"] = (time.time() - started_at) > ttl
|
|
183
|
+
return status
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def authorize_operation(op: OperationRequest) -> str:
|
|
187
|
+
"""Authorize *op* against the persisted session, returning an approval token.
|
|
188
|
+
|
|
189
|
+
Raises :class:`WalletBackendError` if there is no active session or the
|
|
190
|
+
operation is denied by the policy gate.
|
|
191
|
+
"""
|
|
192
|
+
with _lock:
|
|
193
|
+
record = _load_record()
|
|
194
|
+
if not record:
|
|
195
|
+
raise WalletBackendError(
|
|
196
|
+
"No active autonomous session. A host must start one with "
|
|
197
|
+
"start_autonomous_session before unattended execution is allowed."
|
|
198
|
+
)
|
|
199
|
+
config = config_from_dict(record.get("config") or {})
|
|
200
|
+
|
|
201
|
+
# Fail closed: if spend caps are configured but the spend amount for
|
|
202
|
+
# this operation could not be determined, deny rather than approve.
|
|
203
|
+
spend_caps_set = any(
|
|
204
|
+
v > 0
|
|
205
|
+
for v in (
|
|
206
|
+
config.spending.max_per_tx_lamports,
|
|
207
|
+
config.spending.max_hourly_lamports,
|
|
208
|
+
config.spending.max_daily_lamports,
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
if op.spend_amount is None and spend_caps_set:
|
|
212
|
+
raise WalletBackendError(
|
|
213
|
+
"autonomous policy denied operation: spend amount could not be "
|
|
214
|
+
"verified while spend limits are configured."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
ledger = SpendingLedger(
|
|
218
|
+
config.spending,
|
|
219
|
+
clock=time.time,
|
|
220
|
+
entries=[(float(ts), int(amt)) for ts, amt in record.get("spend_entries") or []],
|
|
221
|
+
)
|
|
222
|
+
engine = AutonomousPolicyEngine(
|
|
223
|
+
config,
|
|
224
|
+
ledger=ledger,
|
|
225
|
+
clock=time.time,
|
|
226
|
+
started_at=float(record.get("started_at") or time.time()),
|
|
227
|
+
operations_used=int(record.get("operations") or 0),
|
|
228
|
+
)
|
|
229
|
+
decision = engine.evaluate(op)
|
|
230
|
+
if not decision.approved or not decision.approval_token:
|
|
231
|
+
raise WalletBackendError(f"autonomous policy denied operation: {decision.reason}")
|
|
232
|
+
|
|
233
|
+
record["operations"] = engine.snapshot()["operations"]
|
|
234
|
+
record["spend_entries"] = [[ts, amt] for ts, amt in engine.export_spend()]
|
|
235
|
+
_write_record(record)
|
|
236
|
+
return decision.approval_token
|