@agentlayer.tech/wallet 0.1.47 → 0.1.49

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.
@@ -4,9 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import json
7
+ import time
7
8
  from typing import Any
8
9
 
9
10
  from agent_wallet.approval import inspect_approval_token, verify_approval_token
11
+ from agent_wallet.autonomous_policy import OperationRequest
10
12
  from agent_wallet.exceptions import ProviderError
11
13
  from agent_wallet.models import AgentToolResult, AgentToolSpec
12
14
  from agent_wallet.providers import x402
@@ -278,6 +280,113 @@ class OpenClawWalletAdapter:
278
280
  ),
279
281
  ]
280
282
 
283
+ @staticmethod
284
+ def _autonomous_signals(summary: dict[str, Any]) -> tuple[str | None, int | None]:
285
+ """Best-effort extraction of (recipient, smallest-unit spend) from a summary.
286
+
287
+ Only raw/base-unit amount fields are trusted for spend accounting; a
288
+ UI-denominated amount is ignored so caps are never applied at the wrong
289
+ scale. When no raw amount is present the spend is reported as ``None``
290
+ (unknown), which the session layer treats as fail-closed when caps are
291
+ configured.
292
+ """
293
+ recipient: str | None = None
294
+ for key in ("to_address", "to", "recipient", "destination", "destination_address", "spender"):
295
+ value = summary.get(key)
296
+ if isinstance(value, str) and value.strip():
297
+ recipient = value.strip()
298
+ break
299
+
300
+ spend: int | None = None
301
+ for key in ("input_amount_raw", "amount_raw", "amount_wei", "amount_lamports", "amount_base_units"):
302
+ value = summary.get(key)
303
+ if value is None:
304
+ continue
305
+ try:
306
+ spend = int(str(value))
307
+ except (TypeError, ValueError):
308
+ continue
309
+ break
310
+ return recipient, spend
311
+
312
+ def _build_session_config_from_args(self, args: dict[str, Any]):
313
+ """Construct an AutonomousSessionConfig from start_autonomous_session args."""
314
+ from agent_wallet.autonomous_policy import AutonomousSessionConfig
315
+ from agent_wallet.spending_limits import SpendingConfig
316
+
317
+ def _str_list(value: Any, field: str) -> list[str]:
318
+ if value is None:
319
+ return []
320
+ if not isinstance(value, list) or any(not isinstance(v, str) or not v.strip() for v in value):
321
+ raise WalletBackendError(f"{field} must be an array of non-empty strings.")
322
+ return [v.strip() for v in value]
323
+
324
+ def _int(value: Any, field: str) -> int:
325
+ if value is None:
326
+ return 0
327
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
328
+ raise WalletBackendError(f"{field} must be a non-negative integer.")
329
+ return value
330
+
331
+ return AutonomousSessionConfig(
332
+ enabled=True,
333
+ allowed_tools=frozenset(_str_list(args.get("allowed_tools"), "allowed_tools")),
334
+ allowed_networks=frozenset(_str_list(args.get("allowed_networks"), "allowed_networks")),
335
+ allow_mainnet=bool(args.get("allow_mainnet", False)),
336
+ allowed_recipients=frozenset(_str_list(args.get("allowed_recipients"), "allowed_recipients")),
337
+ allow_any_recipient=bool(args.get("allow_any_recipient", False)),
338
+ require_simulation=bool(args.get("require_simulation", True)),
339
+ spending=SpendingConfig(
340
+ max_per_tx_lamports=_int(args.get("max_per_tx_lamports"), "max_per_tx_lamports"),
341
+ max_hourly_lamports=_int(args.get("max_hourly_lamports"), "max_hourly_lamports"),
342
+ max_daily_lamports=_int(args.get("max_daily_lamports"), "max_daily_lamports"),
343
+ max_txs_per_minute=_int(args.get("max_txs_per_minute"), "max_txs_per_minute"),
344
+ ),
345
+ max_operations=_int(args.get("max_operations"), "max_operations"),
346
+ session_ttl_seconds=_int(args.get("session_ttl_seconds"), "session_ttl_seconds"),
347
+ approval_ttl_seconds=_int(args.get("approval_ttl_seconds"), "approval_ttl_seconds") or 120,
348
+ )
349
+
350
+ async def _authorize_base_swap_permission(
351
+ self,
352
+ *,
353
+ active_backend: AgentWalletBackend,
354
+ tool_name: str,
355
+ action_label: str,
356
+ preview_kwargs: dict[str, Any],
357
+ preview_method: Any,
358
+ ) -> tuple[str, dict[str, Any]]:
359
+ """Authorize one Base swap through the high-trust permission toggle."""
360
+ from agent_wallet import autonomous_permissions
361
+
362
+ network = str(getattr(active_backend, "network", "unknown")).strip().lower()
363
+ if network != autonomous_permissions.BASE_SWAP_NETWORK:
364
+ raise WalletBackendError(
365
+ "Autonomous Base swap permission only applies on network=base. "
366
+ "Use set_evm_network or the network parameter to select Base."
367
+ )
368
+ if not autonomous_permissions.is_base_swap_approved():
369
+ raise WalletBackendError(
370
+ "Autonomous Base swap permission is not enabled. Ask the user to run "
371
+ "agentlayer_autonomous_approve with scope=base_swaps first."
372
+ )
373
+
374
+ preview = await preview_method(**preview_kwargs)
375
+ annotated_preview = self._annotate_sensitive_payload(
376
+ preview,
377
+ action_label=action_label,
378
+ mode="preview",
379
+ )
380
+ summary = dict(annotated_preview.get("confirmation_summary") or {})
381
+ if not summary:
382
+ raise WalletBackendError("Autonomous Base swap preview did not produce a confirmation_summary.")
383
+ token = autonomous_permissions.authorize_base_swap(
384
+ tool_name=tool_name,
385
+ network=network,
386
+ summary=summary,
387
+ )
388
+ return token, summary
389
+
281
390
  def _require_execute_approval(
282
391
  self,
283
392
  *,
@@ -288,10 +397,31 @@ class OpenClawWalletAdapter:
288
397
  backend: AgentWalletBackend | None = None,
289
398
  ) -> None:
290
399
  active_backend = backend or self.backend
400
+ network = str(getattr(active_backend, "network", "unknown"))
291
401
  if not isinstance(approval_token, str) or not approval_token.strip():
292
- raise WalletBackendError(
293
- f"{action_label} execution requires a host-issued approval_token."
294
- )
402
+ # No host token: fall back to the autonomous policy engine if (and
403
+ # only if) a human-authorized session envelope is active. The engine
404
+ # mints the same signed token the host would, so verification below
405
+ # is identical for both paths.
406
+ from agent_wallet import autonomous_session
407
+
408
+ if autonomous_session.is_active():
409
+ recipient, spend = self._autonomous_signals(summary)
410
+ approval_token = autonomous_session.authorize_operation(
411
+ OperationRequest(
412
+ tool_name=tool_name,
413
+ network=network,
414
+ summary=summary,
415
+ spend_amount=spend,
416
+ recipient=recipient,
417
+ simulated=True,
418
+ )
419
+ )
420
+ else:
421
+ raise WalletBackendError(
422
+ f"{action_label} execution requires a host-issued approval_token "
423
+ "(or an active autonomous session authorized by the host)."
424
+ )
295
425
  verify_approval_token(
296
426
  approval_token.strip(),
297
427
  tool_name=tool_name,
@@ -371,6 +501,24 @@ class OpenClawWalletAdapter:
371
501
  "spend_policy": payload.get("spend_policy"),
372
502
  }
373
503
 
504
+ if asset_type == "kamino-lend-intent":
505
+ summary = {
506
+ "operation": action_label,
507
+ "network": str(payload.get("network") or getattr(self.backend, "network", "unknown")),
508
+ "owner": payload.get("owner"),
509
+ "kamino_operation": payload.get("kamino_operation"),
510
+ "market": payload.get("market"),
511
+ "reserve": payload.get("reserve"),
512
+ "amount_ui": payload.get("amount_ui"),
513
+ "recipient_policy": payload.get("recipient_policy"),
514
+ "spend_policy": payload.get("spend_policy"),
515
+ "valid_until_epoch_seconds": payload.get("valid_until_epoch_seconds"),
516
+ }
517
+ obligation_address = payload.get("obligation_address")
518
+ if obligation_address is not None:
519
+ summary["obligation_address"] = obligation_address
520
+ return summary
521
+
374
522
  if asset_type == "solana-lifi-cross-chain-swap":
375
523
  return {
376
524
  "operation": action_label,
@@ -1804,7 +1952,8 @@ class OpenClawWalletAdapter:
1804
1952
  name="swap_evm_tokens",
1805
1953
  description=(
1806
1954
  "Preview, prepare, or execute an ERC-20 or native ETH swap through Velora on supported EVM mainnet networks. "
1807
- "Prepare returns an execution plan only, and execute requires a host-issued approval token bound to the previewed operation."
1955
+ "Prepare returns an execution plan only, and execute requires a host-issued approval token bound to the previewed operation "
1956
+ "unless high-trust autonomous Base swaps are enabled with scope=base_swaps."
1808
1957
  ),
1809
1958
  input_schema={
1810
1959
  "type": "object",
@@ -1959,7 +2108,8 @@ class OpenClawWalletAdapter:
1959
2108
  description=(
1960
2109
  "Preview, prepare, or execute an ERC-20 or native ETH swap through the Uniswap Trading API "
1961
2110
  "(CLASSIC routing) on ethereum or base. ERC-20 inputs use Permit2 EIP-712 signing automatically. "
1962
- "Prepare returns an execution plan only. Execute requires a host-issued approval token bound to the previewed operation."
2111
+ "Prepare returns an execution plan only. Execute requires a host-issued approval token bound to the previewed operation "
2112
+ "unless high-trust autonomous Base swaps are enabled with scope=base_swaps."
1963
2113
  ),
1964
2114
  input_schema={
1965
2115
  "type": "object",
@@ -2037,6 +2187,7 @@ class OpenClawWalletAdapter:
2037
2187
  ),
2038
2188
  )
2039
2189
 
2190
+ tools.extend(self._autonomous_permission_tool_specs())
2040
2191
  return tools
2041
2192
 
2042
2193
  if capabilities.chain == "bitcoin":
@@ -3000,12 +3151,16 @@ class OpenClawWalletAdapter:
3000
3151
  },
3001
3152
  "mode": {
3002
3153
  "type": "string",
3003
- "enum": ["preview", "prepare", "execute"],
3004
- "description": "preview returns a summary, prepare returns an execution plan without signed transaction bytes, execute attempts to submit the Kamino deposit transaction.",
3154
+ "enum": ["preview", "prepare", "execute", "intent_preview", "intent_execute"],
3155
+ "description": "Prefer intent_preview then intent_execute after explicit chat confirmation: intent_execute re-derives the Kamino transaction and executes within the approved parameters without round-tripping the preview payload. Legacy preview/prepare/execute remains supported.",
3156
+ },
3157
+ "valid_for_seconds": {
3158
+ "type": "integer",
3159
+ "description": "Optional intent validity window in seconds for intent_preview (1-300, default 120).",
3005
3160
  },
3006
3161
  "purpose": {"type": "string", "description": "Short explanation of why the deposit is being made."},
3007
3162
  "user_intent": {"type": "boolean", "description": "Must be true for prepare mode."},
3008
- "approval_token": {"type": "string", "description": "Host-issued approval token required for execute mode."},
3163
+ "approval_token": {"type": "string", "description": "Host-issued approval token required for execute/intent_execute mode."},
3009
3164
  },
3010
3165
  "required": ["market", "reserve", "amount_ui", "mode", "purpose"],
3011
3166
  "additionalProperties": False,
@@ -3038,12 +3193,16 @@ class OpenClawWalletAdapter:
3038
3193
  },
3039
3194
  "mode": {
3040
3195
  "type": "string",
3041
- "enum": ["preview", "prepare", "execute"],
3042
- "description": "preview returns a summary, prepare returns an execution plan without signed transaction bytes, execute attempts to submit the Kamino withdraw transaction.",
3196
+ "enum": ["preview", "prepare", "execute", "intent_preview", "intent_execute"],
3197
+ "description": "Prefer intent_preview then intent_execute after explicit chat confirmation: intent_execute re-derives the Kamino transaction and executes within the approved parameters without round-tripping the preview payload. Legacy preview/prepare/execute remains supported.",
3198
+ },
3199
+ "valid_for_seconds": {
3200
+ "type": "integer",
3201
+ "description": "Optional intent validity window in seconds for intent_preview (1-300, default 120).",
3043
3202
  },
3044
3203
  "purpose": {"type": "string", "description": "Short explanation of why the withdraw is being made."},
3045
3204
  "user_intent": {"type": "boolean", "description": "Must be true for prepare mode."},
3046
- "approval_token": {"type": "string", "description": "Host-issued approval token required for execute mode."},
3205
+ "approval_token": {"type": "string", "description": "Host-issued approval token required for execute/intent_execute mode."},
3047
3206
  },
3048
3207
  "required": ["market", "reserve", "amount_ui", "mode", "purpose"],
3049
3208
  "additionalProperties": False,
@@ -3076,12 +3235,16 @@ class OpenClawWalletAdapter:
3076
3235
  },
3077
3236
  "mode": {
3078
3237
  "type": "string",
3079
- "enum": ["preview", "prepare", "execute"],
3080
- "description": "preview returns a summary, prepare returns an execution plan without signed transaction bytes, execute attempts to submit the Kamino borrow transaction.",
3238
+ "enum": ["preview", "prepare", "execute", "intent_preview", "intent_execute"],
3239
+ "description": "Prefer intent_preview then intent_execute after explicit chat confirmation: intent_execute re-derives the Kamino transaction and executes within the approved parameters without round-tripping the preview payload. Legacy preview/prepare/execute remains supported.",
3240
+ },
3241
+ "valid_for_seconds": {
3242
+ "type": "integer",
3243
+ "description": "Optional intent validity window in seconds for intent_preview (1-300, default 120).",
3081
3244
  },
3082
3245
  "purpose": {"type": "string", "description": "Short explanation of why the borrow is being made."},
3083
3246
  "user_intent": {"type": "boolean", "description": "Must be true for prepare mode."},
3084
- "approval_token": {"type": "string", "description": "Host-issued approval token required for execute mode."},
3247
+ "approval_token": {"type": "string", "description": "Host-issued approval token required for execute/intent_execute mode."},
3085
3248
  },
3086
3249
  "required": ["market", "reserve", "amount_ui", "mode", "purpose"],
3087
3250
  "additionalProperties": False,
@@ -3114,12 +3277,16 @@ class OpenClawWalletAdapter:
3114
3277
  },
3115
3278
  "mode": {
3116
3279
  "type": "string",
3117
- "enum": ["preview", "prepare", "execute"],
3118
- "description": "preview returns a summary, prepare returns an execution plan without signed transaction bytes, execute attempts to submit the Kamino repay transaction.",
3280
+ "enum": ["preview", "prepare", "execute", "intent_preview", "intent_execute"],
3281
+ "description": "Prefer intent_preview then intent_execute after explicit chat confirmation: intent_execute re-derives the Kamino transaction and executes within the approved parameters without round-tripping the preview payload. Legacy preview/prepare/execute remains supported.",
3282
+ },
3283
+ "valid_for_seconds": {
3284
+ "type": "integer",
3285
+ "description": "Optional intent validity window in seconds for intent_preview (1-300, default 120).",
3119
3286
  },
3120
3287
  "purpose": {"type": "string", "description": "Short explanation of why the repay is being made."},
3121
3288
  "user_intent": {"type": "boolean", "description": "Must be true for prepare mode."},
3122
- "approval_token": {"type": "string", "description": "Host-issued approval token required for execute mode."},
3289
+ "approval_token": {"type": "string", "description": "Host-issued approval token required for execute/intent_execute mode."},
3123
3290
  },
3124
3291
  "required": ["market", "reserve", "amount_ui", "mode", "purpose"],
3125
3292
  "additionalProperties": False,
@@ -3259,6 +3426,176 @@ class OpenClawWalletAdapter:
3259
3426
  )
3260
3427
  )
3261
3428
 
3429
+ tools.append(
3430
+ AgentToolSpec(
3431
+ name="get_autonomous_session",
3432
+ description=(
3433
+ "Return the status of the current autonomous execution session, if any: "
3434
+ "whether it is active, its allow-lists, spend limits, operation count, and expiry. "
3435
+ "Read-only."
3436
+ ),
3437
+ input_schema={
3438
+ "type": "object",
3439
+ "properties": {},
3440
+ "additionalProperties": False,
3441
+ },
3442
+ read_only=True,
3443
+ risk_level="low",
3444
+ )
3445
+ )
3446
+ tools.append(
3447
+ AgentToolSpec(
3448
+ name="start_autonomous_session",
3449
+ description=(
3450
+ "Start an autonomous execution session so subsequent wallet writes can execute "
3451
+ "WITHOUT a per-transaction human approval, bounded by the supplied limits. "
3452
+ "This grants standing authority, so it requires a host-issued approval_token bound "
3453
+ "to the exact session policy; an agent cannot start a session on its own. "
3454
+ "Preview the policy first (mode=preview) to obtain the confirmation summary the host signs."
3455
+ ),
3456
+ input_schema={
3457
+ "type": "object",
3458
+ "properties": {
3459
+ "mode": {
3460
+ "type": "string",
3461
+ "enum": ["preview", "execute"],
3462
+ "description": "preview returns the policy summary to be confirmed; execute starts the session.",
3463
+ },
3464
+ "allowed_tools": {
3465
+ "type": "array",
3466
+ "items": {"type": "string"},
3467
+ "description": "Wallet tools the agent may execute autonomously.",
3468
+ },
3469
+ "allowed_networks": {
3470
+ "type": "array",
3471
+ "items": {"type": "string"},
3472
+ "description": "Networks the agent may operate on autonomously.",
3473
+ },
3474
+ "allow_mainnet": {
3475
+ "type": "boolean",
3476
+ "description": "Must be true to permit autonomous execution on real-money networks.",
3477
+ },
3478
+ "allowed_recipients": {
3479
+ "type": "array",
3480
+ "items": {"type": "string"},
3481
+ "description": "Destination addresses the agent may send to.",
3482
+ },
3483
+ "allow_any_recipient": {
3484
+ "type": "boolean",
3485
+ "description": "Allow destinations not on the allow-list (spend caps and simulation still apply).",
3486
+ },
3487
+ "require_simulation": {
3488
+ "type": "boolean",
3489
+ "description": "Require a passing simulation before each autonomous execute (default true).",
3490
+ },
3491
+ "max_per_tx_lamports": {"type": "integer", "description": "Per-transaction spend cap in smallest units (0 = unlimited)."},
3492
+ "max_hourly_lamports": {"type": "integer", "description": "Hourly cumulative spend cap (0 = unlimited)."},
3493
+ "max_daily_lamports": {"type": "integer", "description": "Daily cumulative spend cap (0 = unlimited)."},
3494
+ "max_txs_per_minute": {"type": "integer", "description": "Transaction rate cap per minute (0 = unlimited)."},
3495
+ "max_operations": {"type": "integer", "description": "Maximum operations the session may approve (0 = unlimited)."},
3496
+ "session_ttl_seconds": {"type": "integer", "description": "Session lifetime in seconds (0 = no expiry)."},
3497
+ "approval_ttl_seconds": {"type": "integer", "description": "TTL applied to each auto-issued approval token (default 120)."},
3498
+ "approval_token": {
3499
+ "type": "string",
3500
+ "description": "Host-issued approval token bound to this session policy. Required for mode=execute.",
3501
+ },
3502
+ },
3503
+ "required": ["mode"],
3504
+ "additionalProperties": False,
3505
+ },
3506
+ read_only=False,
3507
+ risk_level="high",
3508
+ )
3509
+ )
3510
+ tools.append(
3511
+ AgentToolSpec(
3512
+ name="stop_autonomous_session",
3513
+ description=(
3514
+ "End the current autonomous execution session. Always allowed; this only removes "
3515
+ "standing authority and returns the wallet to per-transaction human approval."
3516
+ ),
3517
+ input_schema={
3518
+ "type": "object",
3519
+ "properties": {},
3520
+ "additionalProperties": False,
3521
+ },
3522
+ read_only=False,
3523
+ risk_level="low",
3524
+ )
3525
+ )
3526
+ tools.append(
3527
+ AgentToolSpec(
3528
+ name="agentlayer_autonomous_status",
3529
+ description=(
3530
+ "Return AgentLayer high-trust autonomous permission status. "
3531
+ "Currently supports only scope=base_swaps."
3532
+ ),
3533
+ input_schema={
3534
+ "type": "object",
3535
+ "properties": {},
3536
+ "additionalProperties": False,
3537
+ },
3538
+ read_only=True,
3539
+ risk_level="low",
3540
+ )
3541
+ )
3542
+ tools.append(
3543
+ AgentToolSpec(
3544
+ name="agentlayer_autonomous_approve",
3545
+ description=(
3546
+ "Enable high-trust autonomous execution for a narrow scope. "
3547
+ "Currently scope=base_swaps lets Base Velora/Uniswap swap execute calls run "
3548
+ "without per-transaction human approval until revoked. This does not cover "
3549
+ "transfers, withdrawals, lending, staking, bridges, Solana swaps, or non-Base networks."
3550
+ ),
3551
+ input_schema={
3552
+ "type": "object",
3553
+ "properties": {
3554
+ "scope": {
3555
+ "type": "string",
3556
+ "enum": ["base_swaps"],
3557
+ "description": "Only base_swaps is currently supported.",
3558
+ },
3559
+ "purpose": {
3560
+ "type": "string",
3561
+ "description": "Short explanation of why the standing permission is being enabled.",
3562
+ },
3563
+ "user_intent": {
3564
+ "type": "boolean",
3565
+ "description": "Must be true after the user explicitly asks to enable this permission.",
3566
+ },
3567
+ },
3568
+ "required": ["scope", "purpose", "user_intent"],
3569
+ "additionalProperties": False,
3570
+ },
3571
+ read_only=False,
3572
+ risk_level="high",
3573
+ )
3574
+ )
3575
+ tools.append(
3576
+ AgentToolSpec(
3577
+ name="agentlayer_autonomous_revoke",
3578
+ description=(
3579
+ "Disable high-trust autonomous execution for a scope. "
3580
+ "Currently supports only scope=base_swaps."
3581
+ ),
3582
+ input_schema={
3583
+ "type": "object",
3584
+ "properties": {
3585
+ "scope": {
3586
+ "type": "string",
3587
+ "enum": ["base_swaps"],
3588
+ "description": "Only base_swaps is currently supported.",
3589
+ }
3590
+ },
3591
+ "required": ["scope"],
3592
+ "additionalProperties": False,
3593
+ },
3594
+ read_only=False,
3595
+ risk_level="low",
3596
+ )
3597
+ )
3598
+
3262
3599
  tools.extend(self._x402_tool_specs())
3263
3600
  return tools
3264
3601
 
@@ -3266,12 +3603,170 @@ class OpenClawWalletAdapter:
3266
3603
  """Return the instruction block to inject into the agent runtime."""
3267
3604
  return WALLET_RUNTIME_INSTRUCTIONS
3268
3605
 
3606
+ @staticmethod
3607
+ def _autonomous_permission_tool_specs() -> list[AgentToolSpec]:
3608
+ return [
3609
+ AgentToolSpec(
3610
+ name="agentlayer_autonomous_status",
3611
+ description=(
3612
+ "Return AgentLayer high-trust autonomous permission status. "
3613
+ "Currently supports only scope=base_swaps."
3614
+ ),
3615
+ input_schema={
3616
+ "type": "object",
3617
+ "properties": {},
3618
+ "additionalProperties": False,
3619
+ },
3620
+ read_only=True,
3621
+ risk_level="low",
3622
+ ),
3623
+ AgentToolSpec(
3624
+ name="agentlayer_autonomous_approve",
3625
+ description=(
3626
+ "Enable high-trust autonomous execution for a narrow scope. "
3627
+ "Currently scope=base_swaps lets Base Velora/Uniswap swap execute calls run "
3628
+ "without per-transaction human approval until revoked. This does not cover "
3629
+ "transfers, withdrawals, lending, staking, bridges, Solana swaps, or non-Base networks."
3630
+ ),
3631
+ input_schema={
3632
+ "type": "object",
3633
+ "properties": {
3634
+ "scope": {
3635
+ "type": "string",
3636
+ "enum": ["base_swaps"],
3637
+ "description": "Only base_swaps is currently supported.",
3638
+ },
3639
+ "purpose": {
3640
+ "type": "string",
3641
+ "description": "Short explanation of why the standing permission is being enabled.",
3642
+ },
3643
+ "user_intent": {
3644
+ "type": "boolean",
3645
+ "description": "Must be true after the user explicitly asks to enable this permission.",
3646
+ },
3647
+ },
3648
+ "required": ["scope", "purpose", "user_intent"],
3649
+ "additionalProperties": False,
3650
+ },
3651
+ read_only=False,
3652
+ risk_level="high",
3653
+ ),
3654
+ AgentToolSpec(
3655
+ name="agentlayer_autonomous_revoke",
3656
+ description=(
3657
+ "Disable high-trust autonomous execution for a scope. "
3658
+ "Currently supports only scope=base_swaps."
3659
+ ),
3660
+ input_schema={
3661
+ "type": "object",
3662
+ "properties": {
3663
+ "scope": {
3664
+ "type": "string",
3665
+ "enum": ["base_swaps"],
3666
+ "description": "Only base_swaps is currently supported.",
3667
+ }
3668
+ },
3669
+ "required": ["scope"],
3670
+ "additionalProperties": False,
3671
+ },
3672
+ read_only=False,
3673
+ risk_level="low",
3674
+ ),
3675
+ ]
3676
+
3269
3677
  async def invoke(self, tool_name: str, arguments: dict[str, Any] | None = None) -> AgentToolResult:
3270
3678
  """Dispatch an agent-facing tool call to the wallet backend."""
3271
3679
  args = arguments or {}
3272
3680
  try:
3273
3681
  active_backend = self._resolve_backend_for_args(args)
3274
3682
 
3683
+ if tool_name == "get_autonomous_session":
3684
+ from agent_wallet import autonomous_session
3685
+
3686
+ return AgentToolResult(tool=tool_name, ok=True, data=autonomous_session.session_status())
3687
+
3688
+ if tool_name == "stop_autonomous_session":
3689
+ from agent_wallet import autonomous_session
3690
+
3691
+ return AgentToolResult(tool=tool_name, ok=True, data=autonomous_session.stop_session())
3692
+
3693
+ if tool_name == "agentlayer_autonomous_status":
3694
+ from agent_wallet import autonomous_permissions
3695
+
3696
+ return AgentToolResult(tool=tool_name, ok=True, data=autonomous_permissions.status())
3697
+
3698
+ if tool_name == "agentlayer_autonomous_approve":
3699
+ from agent_wallet import autonomous_permissions
3700
+
3701
+ scope = str(args.get("scope") or "").strip()
3702
+ purpose = args.get("purpose")
3703
+ if scope != autonomous_permissions.BASE_SWAP_SCOPE:
3704
+ raise WalletBackendError("Only scope=base_swaps is currently supported.")
3705
+ if not isinstance(purpose, str) or not purpose.strip():
3706
+ raise WalletBackendError("purpose is required.")
3707
+ if args.get("user_intent") is not True:
3708
+ raise WalletBackendError(
3709
+ "agentlayer_autonomous_approve requires user_intent=true after the user explicitly asks for this permission."
3710
+ )
3711
+ return AgentToolResult(
3712
+ tool=tool_name,
3713
+ ok=True,
3714
+ data=autonomous_permissions.approve_base_swaps(approved_by="agentlayer_autonomous_approve"),
3715
+ )
3716
+
3717
+ if tool_name == "agentlayer_autonomous_revoke":
3718
+ from agent_wallet import autonomous_permissions
3719
+
3720
+ scope = str(args.get("scope") or "").strip()
3721
+ if scope != autonomous_permissions.BASE_SWAP_SCOPE:
3722
+ raise WalletBackendError("Only scope=base_swaps is currently supported.")
3723
+ return AgentToolResult(
3724
+ tool=tool_name,
3725
+ ok=True,
3726
+ data=autonomous_permissions.revoke_base_swaps(),
3727
+ )
3728
+
3729
+ if tool_name == "start_autonomous_session":
3730
+ from agent_wallet import autonomous_session
3731
+ from agent_wallet.autonomous_session import config_to_dict
3732
+ from agent_wallet.nonce_registry import require_single_use
3733
+
3734
+ config = self._build_session_config_from_args(args)
3735
+ policy_summary = {"operation": "start_autonomous_session", **config_to_dict(config)}
3736
+ mode = str(args.get("mode") or "").strip().lower()
3737
+ if mode not in {"preview", "execute"}:
3738
+ raise WalletBackendError("mode must be 'preview' or 'execute'.")
3739
+ if mode == "preview":
3740
+ return AgentToolResult(
3741
+ tool=tool_name,
3742
+ ok=True,
3743
+ data={
3744
+ "mode": "preview",
3745
+ "confirmation_summary": policy_summary,
3746
+ "execute_requires_approval_token": True,
3747
+ "approval_hint": {"host_must_issue_token_for": policy_summary},
3748
+ },
3749
+ )
3750
+ # execute: starting a session grants standing authority, so it
3751
+ # always requires a genuine host token — never the autonomous
3752
+ # fallback (an agent must not be able to widen its own envelope).
3753
+ approval_token = args.get("approval_token")
3754
+ if not isinstance(approval_token, str) or not approval_token.strip():
3755
+ raise WalletBackendError(
3756
+ "start_autonomous_session execute requires a host-issued approval_token "
3757
+ "bound to the previewed session policy."
3758
+ )
3759
+ verify_approval_token(
3760
+ approval_token.strip(),
3761
+ tool_name="start_autonomous_session",
3762
+ network=str(getattr(active_backend, "network", "unknown")),
3763
+ summary=policy_summary,
3764
+ require_mainnet_confirmation=bool(config.allow_mainnet),
3765
+ )
3766
+ require_single_use(approval_token.strip())
3767
+ status = autonomous_session.start_session(config)
3768
+ return AgentToolResult(tool=tool_name, ok=True, data=status)
3769
+
3275
3770
  if tool_name == "x402_search_services":
3276
3771
  query = args.get("query")
3277
3772
  discovery_provider = args.get("discovery_provider", "auto")
@@ -4368,16 +4863,26 @@ class OpenClawWalletAdapter:
4368
4863
  ),
4369
4864
  )
4370
4865
 
4371
- approval_payload = inspect_approval_token(
4372
- approval_token,
4373
- tool_name=tool_name,
4374
- network=str(getattr(active_backend, "network", "unknown")),
4375
- require_mainnet_confirmation=self._is_mainnet_for_backend(active_backend),
4376
- )
4377
- approval_summary = approval_payload.get("binding", {}).get("summary")
4378
- if not isinstance(approval_summary, dict):
4379
- raise WalletBackendError(
4380
- "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4866
+ if isinstance(approval_token, str) and approval_token.strip():
4867
+ approval_payload = inspect_approval_token(
4868
+ approval_token,
4869
+ tool_name=tool_name,
4870
+ network=str(getattr(active_backend, "network", "unknown")),
4871
+ require_mainnet_confirmation=self._is_mainnet_for_backend(active_backend),
4872
+ )
4873
+ approval_summary = approval_payload.get("binding", {}).get("summary")
4874
+ if not isinstance(approval_summary, dict):
4875
+ raise WalletBackendError(
4876
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4877
+ )
4878
+ approval_summary_copy = dict(approval_summary)
4879
+ else:
4880
+ approval_token, approval_summary_copy = await self._authorize_base_swap_permission(
4881
+ active_backend=active_backend,
4882
+ tool_name=tool_name,
4883
+ action_label="EVM swap",
4884
+ preview_kwargs=preview_kwargs,
4885
+ preview_method=active_backend.preview_evm_swap,
4381
4886
  )
4382
4887
  expected_summary = {
4383
4888
  "operation": "EVM swap",
@@ -4387,12 +4892,11 @@ class OpenClawWalletAdapter:
4387
4892
  "input_amount_raw": amount_in_raw.strip(),
4388
4893
  }
4389
4894
  for key, expected_value in expected_summary.items():
4390
- if approval_summary.get(key) != expected_value:
4895
+ if approval_summary_copy.get(key) != expected_value:
4391
4896
  raise WalletBackendError(
4392
4897
  "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4393
4898
  )
4394
4899
 
4395
- approval_summary_copy = dict(approval_summary)
4396
4900
  self._require_execute_approval(
4397
4901
  approval_token=approval_token,
4398
4902
  tool_name=tool_name,
@@ -4674,16 +5178,26 @@ class OpenClawWalletAdapter:
4674
5178
  ),
4675
5179
  )
4676
5180
 
4677
- approval_payload = inspect_approval_token(
4678
- approval_token,
4679
- tool_name=tool_name,
4680
- network=str(getattr(active_backend, "network", "unknown")),
4681
- require_mainnet_confirmation=self._is_mainnet_for_backend(active_backend),
4682
- )
4683
- approval_summary = approval_payload.get("binding", {}).get("summary")
4684
- if not isinstance(approval_summary, dict):
4685
- raise WalletBackendError(
4686
- "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
5181
+ if isinstance(approval_token, str) and approval_token.strip():
5182
+ approval_payload = inspect_approval_token(
5183
+ approval_token,
5184
+ tool_name=tool_name,
5185
+ network=str(getattr(active_backend, "network", "unknown")),
5186
+ require_mainnet_confirmation=self._is_mainnet_for_backend(active_backend),
5187
+ )
5188
+ approval_summary = approval_payload.get("binding", {}).get("summary")
5189
+ if not isinstance(approval_summary, dict):
5190
+ raise WalletBackendError(
5191
+ "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
5192
+ )
5193
+ approval_summary_copy = dict(approval_summary)
5194
+ else:
5195
+ approval_token, approval_summary_copy = await self._authorize_base_swap_permission(
5196
+ active_backend=active_backend,
5197
+ tool_name=tool_name,
5198
+ action_label="Uniswap swap",
5199
+ preview_kwargs=preview_kwargs,
5200
+ preview_method=active_backend.preview_uniswap_swap,
4687
5201
  )
4688
5202
  expected_summary = {
4689
5203
  "operation": "Uniswap swap",
@@ -4693,12 +5207,11 @@ class OpenClawWalletAdapter:
4693
5207
  "input_amount_raw": amount_in_raw.strip(),
4694
5208
  }
4695
5209
  for key, expected_value in expected_summary.items():
4696
- if approval_summary.get(key) != expected_value:
5210
+ if approval_summary_copy.get(key) != expected_value:
4697
5211
  raise WalletBackendError(
4698
5212
  "approval_token does not match the requested operation. Generate a new approval after previewing the exact action."
4699
5213
  )
4700
5214
 
4701
- approval_summary_copy = dict(approval_summary)
4702
5215
  self._require_execute_approval(
4703
5216
  approval_token=approval_token,
4704
5217
  tool_name=tool_name,
@@ -5480,6 +5993,7 @@ class OpenClawWalletAdapter:
5480
5993
  purpose = args.get("purpose")
5481
5994
  user_intent = args.get("user_intent", False)
5482
5995
  approval_token = args.get("approval_token")
5996
+ valid_for_seconds = args.get("valid_for_seconds", 120)
5483
5997
 
5484
5998
  if not isinstance(market, str) or not market.strip():
5485
5999
  raise WalletBackendError("market is required.")
@@ -5489,10 +6003,18 @@ class OpenClawWalletAdapter:
5489
6003
  raise WalletBackendError("amount_ui is required.")
5490
6004
  if obligation_address is not None and not isinstance(obligation_address, str):
5491
6005
  raise WalletBackendError("obligation_address must be a string when provided.")
5492
- if mode not in {"preview", "prepare", "execute"}:
5493
- raise WalletBackendError("mode must be 'preview', 'prepare' or 'execute'.")
6006
+ if mode not in {"preview", "prepare", "execute", "intent_preview", "intent_execute"}:
6007
+ raise WalletBackendError(
6008
+ "mode must be 'preview', 'prepare', 'execute', 'intent_preview' or 'intent_execute'."
6009
+ )
5494
6010
  if not isinstance(purpose, str) or not purpose.strip():
5495
6011
  raise WalletBackendError("purpose is required.")
6012
+ if mode == "intent_preview" and (
6013
+ not isinstance(valid_for_seconds, int)
6014
+ or valid_for_seconds <= 0
6015
+ or valid_for_seconds > 300
6016
+ ):
6017
+ raise WalletBackendError("valid_for_seconds must be an integer between 1 and 300.")
5496
6018
 
5497
6019
  action_label_map = {
5498
6020
  "kamino_lend_deposit": "Kamino deposit",
@@ -5500,12 +6022,24 @@ class OpenClawWalletAdapter:
5500
6022
  "kamino_lend_borrow": "Kamino borrow",
5501
6023
  "kamino_lend_repay": "Kamino repay",
5502
6024
  }
6025
+ intent_action_label_map = {
6026
+ "kamino_lend_deposit": "Kamino deposit intent",
6027
+ "kamino_lend_withdraw": "Kamino withdraw intent",
6028
+ "kamino_lend_borrow": "Kamino borrow intent",
6029
+ "kamino_lend_repay": "Kamino repay intent",
6030
+ }
5503
6031
  preview_method_map = {
5504
6032
  "kamino_lend_deposit": self.backend.preview_kamino_lend_deposit,
5505
6033
  "kamino_lend_withdraw": self.backend.preview_kamino_lend_withdraw,
5506
6034
  "kamino_lend_borrow": self.backend.preview_kamino_lend_borrow,
5507
6035
  "kamino_lend_repay": self.backend.preview_kamino_lend_repay,
5508
6036
  }
6037
+ intent_preview_method_map = {
6038
+ "kamino_lend_deposit": self.backend.preview_kamino_lend_deposit_intent,
6039
+ "kamino_lend_withdraw": self.backend.preview_kamino_lend_withdraw_intent,
6040
+ "kamino_lend_borrow": self.backend.preview_kamino_lend_borrow_intent,
6041
+ "kamino_lend_repay": self.backend.preview_kamino_lend_repay_intent,
6042
+ }
5509
6043
  execute_method_map = {
5510
6044
  "kamino_lend_deposit": self.backend.execute_kamino_lend_deposit,
5511
6045
  "kamino_lend_withdraw": self.backend.execute_kamino_lend_withdraw,
@@ -5513,8 +6047,113 @@ class OpenClawWalletAdapter:
5513
6047
  "kamino_lend_repay": self.backend.execute_kamino_lend_repay,
5514
6048
  }
5515
6049
  action_label = action_label_map[tool_name]
6050
+ intent_action_label = intent_action_label_map[tool_name]
5516
6051
  preview_method = preview_method_map[tool_name]
6052
+ intent_preview_method = intent_preview_method_map[tool_name]
5517
6053
  execute_method = execute_method_map[tool_name]
6054
+ normalized_obligation_address = (
6055
+ obligation_address.strip()
6056
+ if isinstance(obligation_address, str) and obligation_address.strip()
6057
+ else None
6058
+ )
6059
+
6060
+ if mode == "intent_preview":
6061
+ intent_preview = await intent_preview_method(
6062
+ market=market.strip(),
6063
+ reserve=reserve.strip(),
6064
+ amount_ui=amount_ui.strip(),
6065
+ obligation_address=normalized_obligation_address,
6066
+ valid_for_seconds=valid_for_seconds,
6067
+ )
6068
+ if bool(intent_preview.get("requires_obligation_address")):
6069
+ raise WalletBackendError(
6070
+ f"{action_label} requires obligation_address when multiple Kamino obligations match the selected position."
6071
+ )
6072
+ return AgentToolResult(
6073
+ tool=tool_name,
6074
+ ok=True,
6075
+ data=self._annotate_sensitive_payload(
6076
+ intent_preview,
6077
+ action_label=intent_action_label,
6078
+ mode="preview",
6079
+ ),
6080
+ )
6081
+
6082
+ if mode == "intent_execute":
6083
+ approval_payload = inspect_approval_token(
6084
+ approval_token,
6085
+ tool_name=tool_name,
6086
+ network=str(getattr(self.backend, "network", "unknown")),
6087
+ require_mainnet_confirmation=self._is_mainnet_for_backend(self.backend),
6088
+ )
6089
+ approval_summary = approval_payload.get("binding", {}).get("summary")
6090
+ if not isinstance(approval_summary, dict):
6091
+ raise WalletBackendError(
6092
+ "approval_token does not match the requested operation. Generate a new intent preview and approval before execute."
6093
+ )
6094
+ expected_summary = {
6095
+ "operation": intent_action_label,
6096
+ "network": str(getattr(self.backend, "network", "unknown")),
6097
+ "market": market.strip(),
6098
+ "reserve": reserve.strip(),
6099
+ "amount_ui": amount_ui.strip(),
6100
+ }
6101
+ for key, expected_value in expected_summary.items():
6102
+ if approval_summary.get(key) != expected_value:
6103
+ raise WalletBackendError(
6104
+ f"approval_token does not match the requested {action_label} intent. Generate a fresh intent preview and approval before execute."
6105
+ )
6106
+ if approval_summary.get("recipient_policy") != "owner-only":
6107
+ raise WalletBackendError("approved Kamino intent recipient policy is invalid.")
6108
+ if approval_summary.get("spend_policy") != "exact-amount":
6109
+ raise WalletBackendError("approved Kamino intent spend policy is invalid.")
6110
+ current_owner = await self.backend.get_address()
6111
+ approved_owner = approval_summary.get("owner")
6112
+ if approved_owner and current_owner and str(approved_owner) != str(current_owner):
6113
+ raise WalletBackendError(
6114
+ "approval_token does not match the active wallet owner. Generate a fresh intent preview and approval before execute."
6115
+ )
6116
+ approved_obligation = approval_summary.get("obligation_address")
6117
+ if (
6118
+ normalized_obligation_address is not None
6119
+ and approved_obligation is not None
6120
+ and str(approved_obligation) != normalized_obligation_address
6121
+ ):
6122
+ raise WalletBackendError(
6123
+ "approval_token does not match the requested obligation. Generate a fresh intent preview and approval before execute."
6124
+ )
6125
+ valid_until = approval_summary.get("valid_until_epoch_seconds")
6126
+ if valid_until is not None and int(time.time()) > int(valid_until):
6127
+ raise WalletBackendError(
6128
+ "Approved Kamino intent has expired. Create a fresh intent preview."
6129
+ )
6130
+ approval_summary_copy = dict(approval_summary)
6131
+ self._require_execute_approval(
6132
+ approval_token=approval_token,
6133
+ tool_name=tool_name,
6134
+ summary=approval_summary_copy,
6135
+ action_label=intent_action_label,
6136
+ )
6137
+ result = await execute_method(
6138
+ market=market.strip(),
6139
+ reserve=reserve.strip(),
6140
+ amount_ui=amount_ui.strip(),
6141
+ obligation_address=(
6142
+ str(approved_obligation).strip()
6143
+ if approved_obligation
6144
+ else normalized_obligation_address
6145
+ ),
6146
+ approved_preview=None,
6147
+ )
6148
+ return AgentToolResult(
6149
+ tool=tool_name,
6150
+ ok=True,
6151
+ data=self._annotate_sensitive_payload(
6152
+ result,
6153
+ action_label=action_label,
6154
+ mode="execute",
6155
+ ),
6156
+ )
5518
6157
 
5519
6158
  if mode == "preview":
5520
6159
  preview = await preview_method(