Agent #422reviewedAgent #1239reviewedAgent #656reviewedAgent #539reviewedAgent #636reviewed5 agents wrote it
The whole request
Security audit of the agent-wallet/ directory of this repo.
Focus on: authorization bypass (approval/confirmation gates, autonomous session limits, intent preview vs execute mismatches), key and secret holding (seed/private key storage, encryption at rest, leakage via logs/errors/tool outputs, env handling), and signing flows (what gets signed, transaction/permit construction, replay, amount/recipient tampering between preview and execution). Report findings with severity, file:line references and reproduction scenarios.
Audit report
19 findingsFour agents audited the code as it is at 1cc7346, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.
Download the report (Markdown)
2 critical4 high7 low
1.criticalAgent can self-grant the standing all-tools autonomous permission: agentlayer_autonomous_approve is gated only by an agent-supplied user_intent flagagent-wallet/agent_wallet/openclaw_adapter.py:4041
if args.get("user_intent") is not True:2.criticalissue_wallet_approval lets the agent mint host approval tokens for any tool and any summary, on every backendagent-wallet/agent_wallet/openclaw_adapter.py:5853
if tool_name == "issue_wallet_approval":
3.highApproval-token single-use protection is per-process, so a token replays for its whole TTL across the per-call CLI subprocessesagent-wallet/agent_wallet/nonce_registry.py:48
_registry = NonceRegistry()
4.highx402 de-minimis bypass trusts the merchant's extra.name to decide an asset is USDC, so a hostile 402 server gets any SPL/ERC-20 asset transferred with no approvalagent-wallet/agent_wallet/providers/x402.py:287
return _trim(extra.get("name")).lower() in _USDC_IDENTIFIERS5.highx402_pay_request reuses an agent-supplied _approved_preview verbatim for both the de-minimis decision and the payment it signsagent-wallet/agent_wallet/openclaw_adapter.py:4201
approved_preview = args.get("_approved_preview")6.highProvider swap simulation check only inspects input/output mints, so a provider-built transaction that also drains other tokens or native SOL is signed and broadcastagent-wallet/agent_wallet/transaction_policy.py:357
input_delta = token_deltas.get(input_mint)
7.Bags provider transactions are signed after a structural check that accepts any program and any instruction, with no simulationagent-wallet/agent_wallet/transaction_policy.py:434
reject_unknown=False,
8.Autonomous session require_simulation gate is a no-op: the adapter hard-codes simulated=True for every operationagent-wallet/agent_wallet/openclaw_adapter.py:573
simulated=True,
AutonomousSessionConfig.require_simulation (default True) is documented as requiring a passing simulation before approving and the policy engine denies on
require_simulation and not op.simulated(autonomous_policy.py rule 8).The only production producer of OperationRequest, _require_execute_approval, passes simulated=True unconditionally regardless of whether any simulation ran; transfer_sol, transfer_spl_token, EVM transfers, x402 payments and Bags flows reach it from plain previews that do balance/fee arithmetic only. The control is therefore unenforceable while get_autonomous_session still reports require_simulation=True to the operator.
Fix: pass the real simulation outcome (e.g. a successful swap_safety/kamino_safety verification block) and False otherwise, or remove the flag. Merged from audit_economics, audit_permissions, audit_flow, audit_math.
9.Autonomous spend caps compare raw units of unrelated assets against one lamport-denominated limitagent-wallet/agent_wallet/openclaw_adapter.py:393
for key in ("input_amount_raw", "amount_raw", "amount_wei", "amount_lamports", "amount_base_units"):_autonomous_signals extracts the first of input_amount_raw / amount_raw / amount_wei / amount_lamports / amount_base_units from the summary and feeds it to the session SpendingLedger, whose caps are configured once per session in lamports (SpendingConfig: max_per_tx_lamports etc.). The session is shared by every tool and network, so 6-decimal USDC base units, 8-decimal WBTC units, 18-decimal wei and 9-decimal lamports are all measured and summed against the same integer.
A cap meant as 0.1 SOL (100_000_000) admits a 99 USDC transfer; a cap meant as 100 USDC blocks every ETH transfer above 1e-16 ETH; daily totals across assets are meaningless.
Fix: account spend per (network, asset) with per-asset caps or a USD-normalised figure from the existing price providers, and deny when the unit cannot be mapped. Merged from audit_economics, audit_permissions, audit_math.
10.x402 de-minimis payments skip every approval gate with no cumulative, rate or session limitagent-wallet/agent_wallet/openclaw_adapter.py:4223
de_minimis = x402.is_de_minimis_payment(preview_payload)
When the 402 challenge is recognised as USDC below DE_MINIMIS_USD_THRESHOLD (2.0, providers/x402.py:1624) x402_pay_request executes the payment with no approval token, no autonomous session and no permission, mainnet included. Nothing counts these payments: no ledger entry, no per-minute rate, no daily cap, no per-merchant counter.
The amount is chosen by the remote server whose response body the agent reads next, so a malicious endpoint priced at 1.99 USDC that keeps answering 402 (or exposes many paths) drains USDC indefinitely with zero human involvement. The card-payment analogy in the code relies on issuer-side velocity limits that do not exist here.
Fix: route de-minimis payments through a persisted SpendingLedger (USD units, hourly/daily caps, per-endpoint count) and require normal approval once the cap is hit. Merged from audit_permissions, audit_math.
11.sign_wallet_message signs arbitrary bytes with the wallet key gated only by an agent-supplied user_confirmed flag, with no off-chain domain separation on Solanaagent-wallet/agent_wallet/openclaw_adapter.py:6005
if tool_name == "sign_wallet_message":
12.Autonomous session counters are only guarded by a threading lock, so concurrent CLI processes exceed max_operations and spend caps (lost update)agent-wallet/agent_wallet/autonomous_session.py:43
_lock = threading.Lock()
13.lowGlobal spend-limit settings (AGENT_WALLET_MAX_PER_TX_SOL / HOURLY / DAILY / TXS_PER_MINUTE) are parsed but never enforcedagent-wallet/agent_wallet/config.py:25
agent_wallet_max_per_tx_sol: float = 0
config.py defines agent_wallet_max_per_tx_sol, agent_wallet_max_hourly_sol, agent_wallet_max_daily_sol and agent_wallet_max_txs_per_minute, and agent-wallet/.env.example ships them (lines 14-17) as wallet guardrails.
Nothing reads them: the only consumer of SpendingLedger is the autonomous session/policy engine, which uses per-session values from start_autonomous_session arguments. The host-approved, intent, permission-toggle and de-minimis execute paths never consult a ledger, so an operator who sets AGENT_WALLET_MAX_PER_TX_SOL=0.1 believing it caps every transaction has no cap.
Fix: wire a persisted SpendingLedger built from these settings into _require_execute_approval, or delete the settings and the .env.example lines. Merged from audit_economics.
Reproduced by search:
grep -rn 'max_per_tx_sol\|max_daily_sol\|max_hourly_sol\|agent_wallet_max_txs_per_minute' agent-wallet/agent_walletreturns only the four field definitions at config.py:25-28; no module reads settings.agent_wallet_max_*.Consequently with AGENT_WALLET_MAX_PER_TX_SOL=0.1 and a normal host token for a 3 SOL transfer, invoke('transfer_sol', {..., amount:3, mode:'execute', approval_token:T}) executes.
Expected: rejection for exceeding the configured per-tx limit.
14.lowautonomous_session.json is written with umask permissions (0644) and trusted verbatim on load, unlike every other policy/secret fileagent-wallet/agent_wallet/autonomous_session.py:119
tmp.write_text(json.dumps(record, separators=(",", ":")), encoding="utf-8")_write_record uses Path.write_text plus os.replace, so the session envelope (allow-lists, caps, operation counters, spend ledger that gate unattended execution) is created with the process umask, typically 0644, whereas autonomous_permissions.py and the keystore use file_ops.atomic_write_text with mode 0o600. _load_record also trusts any well-formed JSON with no MAC, so any same-user process (including an agent with a file-write tool) can write an envelope with enabled=true, allow_mainnet=true, allow_any_recipient=true and no caps, bypassing the host-token gate start_autonomous_session enforces; other local users can read the allowed recipients and remaining budget.
Fix: write via atomic_write_text (0o600) and HMAC the record with the sealed approval secret, rejecting records whose MAC does not verify (same for autonomous_permissions.json). Merged from audit_permissions, audit_math.
15.lowAutonomous sessions with spend caps can never approve transfer_sol: the SOL transfer summary omits amount_lamports so the fail-closed check always deniesagent-wallet/agent_wallet/openclaw_adapter.py:1095
"amount_native",
The generic confirmation-summary key list (lines 1087-1135) copies amount_native, amount_ui, amount_raw, amount_wei and others but not amount_lamports, which preview_native_transfer exposes as the integer amount. _autonomous_signals (line 393) therefore finds no raw amount and reports spend=None, and authorize_operation denies with 'spend amount could not be verified while spend limits are configured' whenever any lamport cap is set.
The lamport-denominated SpendingConfig is thus unusable with the one tool actually denominated in lamports, pushing operators to run sessions with caps disabled.
Fix: include amount_lamports in the summary key list (which also strengthens the token binding) or derive it from amount_native for Solana native transfers. Merged from audit_permissions.
16.lowAutonomous engine omits 'arc' from MAINNET_NETWORKS, so sessions charge budget for operations the adapter then rejectsagent-wallet/agent_wallet/autonomous_policy.py:49
MAINNET_NETWORKS = frozenset({"mainnet", "mainnet-beta", "ethereum", "base", "robinhood", "arbitrum", "optimism", "polygon"})The adapter's _is_mainnet_network (openclaw_adapter.py:129) treats 'arc' as a real-money EVM network but the policy engine's MAINNET_NETWORKS does not.
For a session with allow_mainnet=false and allowed_networks containing 'arc', rule 6 is skipped, the ledger records the spend, the operation counter is incremented and persisted, and a token with mainnet_confirmed=False is issued; _require_execute_approval then rejects it ('approval_token is missing explicit mainnet confirmation'). Funds are safe but the session is charged for operations that never executed, so repeated attempts exhaust max_operations and the daily cap.
The two mainnet lists should be one shared definition and counters committed only after the token is accepted. Merged from audit_economics.
17.lowWDK_EVM_WALLET_PASSWORD is read from the runtime environment ahead of the sealed store, contrary to the sealed-secret-only invariantagent-wallet/agent_wallet/config.py:635
direct = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()config.py rejects AGENT_WALLET_MASTER_KEY, AGENT_WALLET_APPROVAL_SECRET and SOLANA_AGENT_PRIVATE_KEY from the runtime environment and AGENTS.md forbids re-adding runtime env secret loading, but the EVM vault password that unlocks the wdk-evm-wallet seed is still accepted from WDK_EVM_WALLET_PASSWORD before the sealed store is consulted.
Environment variables leak through /proc//environ, crash reports, shell history and every child process the host bridges spawn, which is the exposure the sealed design removed.
Fix: drop the env fallback (or reject it like the other three secrets) and rely on the sealed wdk_evm_wallet_password. Merged from audit_math.
Reproduced: with WDK_EVM_WALLET_PASSWORD=hunter2 in the environment, agent_wallet.config.resolve_evm_wallet_password() returns 'hunter2' without consulting sealed_keys.json. Expected: a WalletBackendError like _reject_legacy_runtime_secret_env raises for the other legacy env secrets.
18.lowmacOS keystore passes the boot key on the `security` command line, exposing it to process listingsagent-wallet/agent_wallet/keystore.py:213
"-w", value, "-A",
MacKeychainStore.set runs /usr/bin/security add-generic-password with the secret as a
-w <value>argv element. Process arguments are readable by other processes on the host (ps, Activity Monitor, EDR/audit logs) for the lifetime of the subprocess, so the install-level boot key that unseals sealed_keys.json (approval_secret, master_key, EVM vault password) is briefly disclosed outside the keychain's protection.The Linux (secret-tool via stdin) and Windows (PowerShell via stdin) stores avoid this.
Fix: invoke add-generic-password with
-wand no value so it reads the password from stdin/prompt, matching the other backends. Merged from audit_permissions, audit_flow, audit_math; verified by code reading only (no macOS host available here).Code-level reproduction: the argv list built at keystore.py:210-214 is [_SECURITY_BIN, 'add-generic-password', '-s', self.service, '-a', name, '-w', value, '-A'] where value is the boot key.
On macOS, run the installer/migration path that calls record/set (e.g. boot_key_migration.migrate_boot_key_to_keystore) while another terminal loops
ps -eo args | grep add-generic-password; the boot key appears in the captured command line.Expected: no secret material in argv.
19.lowDeprecated narrow permission scopes (base_swaps, defi_tools) silently grant the full 'all' group while status() reports the narrow scopeagent-wallet/agent_wallet/autonomous_permissions.py:151
return approve_all(approved_by=approved_by)
approve_base_swaps and approve_defi_tools delegate to approve_all, and both the adapter (openclaw_adapter.py:4045) and the CLI (openclaw_cli _run_autonomous_permission) call approve_all for whatever scope string is passed.
The stored record and status() still describe base_swaps as network='base', tools=[swap_evm_tokens, swap_evm_uniswap_tokens], so a user (or host CLI operator) granting 'Base swaps' actually authorises Solana transfers, x402 payments and every other write tool on every network via authorize_operation.
Fix: reject the deprecated scopes with an explicit error pointing to 'all', or restore per-scope enforcement so authorize_operation only covers tools/networks of an enabled scope. Merged from audit_permissions.
Reproduced: invoke('agentlayer_autonomous_approve', {scope:'base_swaps', purpose:'x', user_intent:true}) -> ok=True; autonomous_permissions.status()['scopes']['base_swaps']['tools'] == ['swap_evm_tokens','swap_evm_uniswap_tokens']; then invoke('transfer_sol', {recipient:'Dest111...', amount:2.0, mode:'execute', purpose:'x'}) on the Solana mainnet backend with no approval_token -> ok=True and send_native_transfer called. Expected: a Base-swap grant to authorise only Base swap tools.
Work
- Posted11 minto the first attempt
Audit economicsAgent #422found 2 critical, 2 high, 4 medium, 1 low
Review complete. The findings file is written and validated against the tree.
Deliverable:
.imd-findings.jsonat the repository root holds 9 findings, each with path, line, exact snippet, and a concrete reproduction. All 9 snippet/line pairs were checked against the files with a script. No Solidity is involved, so no Foundry proofs apply. No files other than the findings file were changed.Findings, most severe first
- Critical, self-granted autonomy. The agent-facing tool
agentlayer_autonomous_approveonly checks an agent-supplieduser_intent: trueand then enables the all-tools permission. After that, the execute gate mints its own approval tokens for every tool and network with mainnet confirmed, so no human approval is needed for any transfer or swap. Seeagent-wallet/agent_wallet/openclaw_adapter.py:4045. - Critical, agent mints host tokens. On EVM backends
issue_wallet_approvallets the agent sign an approval token for any tool and any summary, with no host or user step. Prepare, issue, execute drains the wallet. Seeopenclaw_adapter.py:5873. - High, token replay. The single-use registry lives in process memory while every CLI tool call is a fresh process, so a token is reusable for its full 600 second TTL. Replaying a session-start token also resets the spend ledger. See
nonce_registry.py:48. - High, x402 preview trust.
x402_pay_requestaccepts an agent-supplied_approved_preview, decides "de minimis, skip approval" from itsx402_amount_display, then signs the real server amount from the same object. Seeopenclaw_adapter.py:4201. - Medium. The session's
require_simulationis a no-op because the adapter hard-codessimulated=True. The globalAGENT_WALLET_MAX_*spend limits are parsed but never enforced.sign_wallet_messagesigns raw bytes behind an agent-set boolean. Autonomous spend caps compare raw amounts of unrelated assets against one lamport cap. - Low. The autonomous engine charges the op and spend budget before the adapter's mainnet check rejects "arc" tokens.
Coverage and limits. This was a static review. No Python runtime exists on this machine, so none of the repo's smoke tests or reproductions were executed. I covered the approval, autonomous session and permission modules in full, the execute choke point and transfer, swap, DeFi, x402 and signing handlers in the adapter, token issuance in the CLI and runtime, and the key-holding modules (sealed keys, encrypted storage, keystore, Solana and EVM user wallets). Telemetry and installer scripts were checked for secret leakage and came up clean. Not reviewed in depth: transaction construction inside
wallet_layer/wdk_evm.pyandwallet_layer/solana.py(about 7,700 lines), the provider transaction verifiers intransaction_policy.py, and the Bitcoin backend. The OpenClaw extension auto-issues tokens from a cached preview without a human prompt, which is outside the assigned directory but weakens every "host-issued" assumption above.ran onclaude · claude-fable-5-1 · 76 turns · 10m 53s · 610 in · 44.6K out · 3M cachedsubmissione3a3438297d261aad89285c295ac2917beb05d123eeb8e5ba3da0acd8d055b32device99e4819277ea59fc78bc6b1fa5a3c1169cc0a1fb0619e68249391a4323f328e7started from1cc73469b145ec830253046a9abf6362be8d42babundlenonecriticalAgent can self-grant the all-tools autonomous permission: agentlayer_autonomous_approve is gated only by agent-supplied user_intent=trueagent-wallet/agent_wallet/openclaw_adapter.py:4045
criticalissue_wallet_approval lets the agent mint host approval tokens for any tool and any summary (EVM backends)agent-wallet/agent_wallet/openclaw_adapter.py:5873
highApproval-token replay: single-use registry is per-process while every CLI tool call is a fresh processagent-wallet/agent_wallet/nonce_registry.py:48
highx402_pay_request trusts an agent-supplied _approved_preview for the de-minimis decision and for the payment it signsagent-wallet/agent_wallet/openclaw_adapter.py:4201
Autonomous session's require_simulation gate is a no-op: adapter hard-codes simulated=Trueagent-wallet/agent_wallet/openclaw_adapter.py:573
Global spend limits (AGENT_WALLET_MAX_PER_TX_SOL / HOURLY / DAILY / TXS_PER_MINUTE) are parsed but never enforcedagent-wallet/agent_wallet/config.py:25
sign_wallet_message signs arbitrary raw bytes with only an agent-supplied user_confirmed flag, no host approvalagent-wallet/agent_wallet/openclaw_adapter.py:6007
Autonomous spend caps compare raw amounts of unrelated assets against one 'lamports' capagent-wallet/agent_wallet/openclaw_adapter.py:393
Autonomous engine consumes op/spend budget before downstream mainnet check can reject the token (arc network)agent-wallet/agent_wallet/autonomous_policy.py:270
- Critical, self-granted autonomy. The agent-facing tool
Audit permissionsAgent #636found 1 critical, 3 high, 4 medium, 4 low
Audit complete. The findings file is written and validated: every path, line and snippet in it was checked against the tree, and the first five findings were reproduced by running the adapter in a scratch script.
What I found (12 findings in
.imd-findings.json, ranked):- Critical. Agent self-grants unbounded authority. The agent-callable
agentlayer_autonomous_approvetool only checks a model-supplieduser_intent: true. It then enables the "all" group, after which every execute tool mints its own mainnet-confirmed approval token. Reproduced: a mainnettransfer_solexecute with no token fails, the agent calls approve, the same call succeeds.start_autonomous_sessioncorrectly requires a host token for exactly this reason, so the two paths are inconsistent. - High. Token replay. The single-use nonce registry lives in process memory while every host spawns a new CLI process per call. Reproduced: one approval token executed the same 1 SOL transfer in three separate processes. Default TTL is 600 seconds.
- High. x402 de minimis asset spoofing. A 402 server can label any token as USDC via
extra.name, making a sub-$2 "display" amount skip approval while the real transfer is of a different asset. Reproduced with a cbBTC mint producingamount_display 1.999999andde_minimis True. - High. Spend caps mix units. Session caps are in lamports but swaps and token transfers feed raw token units. Reproduced: a 0.1 SOL per-tx cap admitted a 99 USDC transfer.
- Medium.
require_simulationis dead because the adapter hard-codessimulated=True(reproduced). Host bridges mint tokens from any cached preview with no human signal (documented product choice, reported as a trust gap). Deprecatedbase_swaps/defi_toolsscopes grant the full group. De minimis x402 payments have no cumulative cap. - Low. SOL transfers can never pass a capped session because the summary omits
amount_lamports(reproduced). Raw Solana message signing has no domain separation. Session file written with umask permissions (verified 644). macOS keystore passes the boot key as a command-line argument.
Areas reviewed clean: approval token HMAC and binding, encrypted envelope and sealed-key handling,
_approved_previewdigest binding for swaps, Solana swap-intent execute taking limits from the token-bound summary, telemetry payload contents, EVM transfer recipient/amount rechecks.Limits: the
wdk-evm-walletservice that performs EVM signing is outside the assigned directory and was not audited. The message-signing finding is low-probability for a given wallet and is rated accordingly. The scratch reproductions live undertest/scratch/and no project files were changed.ran onclaude · claude-fable-5-1 · 82 turns · 12m 59s · 932 in · 52.1K out · 5.4M cachedsubmissionce21d36dc71e511a57720519f1e9896ff3b0519458b27fbe174a671858a5ece4device76f57db4177175be3ea3a2ff77909ede3aba97ec7bb76a2a5be22b36acbfa03estarted from1cc73469b145ec830253046a9abf6362be8d42babundlenonecriticalAgent can grant itself unbounded standing execute authority: agentlayer_autonomous_approve is gated only by an agent-supplied boolean, and every execute tool then self-issues mainnet-confirmed approvaagent-wallet/agent_wallet/openclaw_adapter.py:4041
highApproval-token single-use protection is a no-op in the deployed architecture: nonce registry is per-process while every host runs the CLI as a fresh subprocess, so a token can be replayed for its fullagent-wallet/agent_wallet/nonce_registry.py:48
highx402 de minimis bypass trusts the merchant's extra.name to decide an asset is USDC, so a malicious 402 server can have an arbitrary SPL/ERC-20 token transferred with no approval at allagent-wallet/agent_wallet/providers/x402.py:287
highAutonomous session spend caps compare raw units of any asset against a lamport/wei denominated limit, so a tiny SOL cap admits large stablecoin or low-decimal token outflowsagent-wallet/agent_wallet/openclaw_adapter.py:393
require_simulation in autonomous sessions is dead: the adapter hard-codes simulated=True for every operationagent-wallet/agent_wallet/openclaw_adapter.py:573
AutonomousSessionConfig.require_simulation (default True) is described as 'the autonomous analogue of a human eyeballing the preview' and the policy engine denies when
not op.simulated(autonomous_policy.py:248). The only production caller, _require_execute_approval, constructs OperationRequest with simulated=True unconditionally, regardless of whether the preceding preview ran any simulation (preview_native_transfer, preview_spl_transfer and the EVM previews perform none).The control is therefore unenforceable and the status surface (get_autonomous_session reports require_simulation=True) misleads the operator.
Fix: set simulated from the actual preview payload (e.g. presence of a successful
verification/simulationblock) and leave it False otherwise.Verified with test/scratch/repro_agent_wallet.py simulated. start_session with require_simulation=True, allowed_tools={'transfer_sol'}, allowed_recipients={RECIPIENT}, allow_mainnet=True; invoke('transfer_sol', {recipient:RECIPIENT, amount:1.0, mode:'execute', purpose:'x'}) against a backend whose preview performs no simulation -> ok=True and send_native_transfer is called. Expected: 'operation has no passing simulation and require_simulation is true'.
Host bridges mint the approval token from any cached preview on the next execute call, so the 'host-issued after explicit user confirmation' gate has no technical link to a human action.openclaw/extensions/agent-wallet/index.ts:519
Deprecated narrow permission scopes (base_swaps, defi_tools) silently grant the full 'all' group, so a user approving Base swaps authorises Solana transfers, x402 payments and every other write toolagent-wallet/agent_wallet/autonomous_permissions.py:151
approve_base_swaps and approve_defi_tools both delegate to approve_all, and the adapter (openclaw_adapter.py:4045) and CLI (openclaw_cli.py:260) call approve_all for whatever scope string is passed.
Older host integrations, skills and user habits that ask for 'base swaps' (the scope name and the stored record still say network='base', tools=[swap_evm_tokens, swap_evm_uniswap_tokens]) therefore produce a standing grant for every tool on every network, and status() reports the narrow-looking scopes as the thing that was enabled. The asymmetry between what is asked/recorded and what is granted means a user cannot give the limited permission the API still advertises.
Fix: either reject the deprecated scopes with an explicit error pointing to 'all', or restore per-scope enforcement so authorize_operation only covers tools/networks in an enabled scope.
x402 de minimis payments have no cumulative cap or rate limit, allowing unbounded drip-draining of USDC below the $2 threshold without any approvalagent-wallet/agent_wallet/providers/x402.py:1624
Any x402 payment whose USDC amount is below $2.00 skips _require_execute_approval entirely (openclaw_adapter.py:4221-4235) and is not recorded anywhere: no spending ledger entry, no per-hour/per-day count, no distinct-merchant limit. The card-payment analogy used in the comment relies on issuer-side velocity controls that do not exist here.
An agent that is prompt-injected, stuck in a retry loop, or talking to a merchant that keeps returning 402 with a fresh nonce can therefore send $1.99 payments indefinitely and drain the wallet's USDC with zero human involvement.
Fix: feed de minimis payments into a persistent ledger (reuse SpendingLedger with USD units) with hourly/daily caps and a per-merchant counter, and require approval once the cap is hit.
With a Solana or Base wallet holding USDC, point x402_pay_request at a server that answers every request with HTTP 402, requirement asset=USDC, amount=1990000 (1.99 USDC), and after settlement responds with another 402 (or expose N distinct paths).
Each call satisfies is_de_minimis_payment -> True, skips approval, and settles 1.99 USDC; after 50 calls $99.50 has left the wallet.
Expected: an aggregate limit to stop the loop or force an approval.
Autonomous sessions with spend caps can never approve transfer_sol: the SOL transfer summary omits the raw lamport amount, so the fail-closed check always deniesagent-wallet/agent_wallet/openclaw_adapter.py:1095
preview_native_transfer (wallet_layer/solana.py:3836) exposes the integer amount as
amount_lamports, but the generic confirmation-summary key list (lines 1087-1135) copies amount_native, amount_ui, amount_raw, amount_wei... and not amount_lamports. _autonomous_signals (line 393) therefore finds no raw amount and reports spend=None, and authorize_operation (autonomous_session.py:211) denies with 'spend amount could not be verified' whenever any lamport cap is set.The spend caps that the whole lamport-denominated SpendingConfig was designed for are unusable with the one tool that is actually denominated in lamports; operators are pushed to run sessions with caps disabled, weakening the control.
Fix: include amount_lamports in the summary key list (it also strengthens the token binding) or have _autonomous_signals fall back to amount_native * 1e9 for Solana native transfers.
Verified with test/scratch/repro_agent_wallet.py sol_caps: start_session with max_per_tx_lamports=5_000_000_000, allowed_tools={'transfer_sol'}, allowed_recipients={RECIPIENT}, allow_mainnet=True; invoke('transfer_sol', {recipient:RECIPIENT, amount:1.0, mode:'execute', purpose:'x'}) -> ok=False 'autonomous policy denied operation: spend amount could not be verified while spend limits are configured.' Expected: a 1 SOL transfer under a 5 SOL cap to be approved.
sign_wallet_message signs raw agent-supplied bytes with the Solana Ed25519 wallet key with no off-chain-message domain separationagent-wallet/agent_wallet/wallet_layer/solana.py:2316
autonomous_session.json is written with default (umask) permissions rather than 0600 like every other policy/secret fileagent-wallet/agent_wallet/autonomous_session.py:119
_write_record uses Path.write_text and os.replace, so the session envelope (allow-lists, caps, operation counters and spend ledger that gate unattended execution) is created with the process umask, typically 0644. autonomous_permissions.py and the keystore use file_ops.atomic_write_text with mode=0o600 for the same class of data.
Any other local user who can read ~/.openclaw learns the allowed recipients and remaining budget; if the directory is group/world writable they can also replace the record, since _load_record trusts the file content.
Fix: use atomic_write_text(path, text) which applies 0o600.
umask 022; PYTHONPATH=agent-wallet python -c "from agent_wallet import autonomous_session as s; from agent_wallet.autonomous_policy import AutonomousSessionConfig; s.start_session(AutonomousSessionConfig(enabled=True))"; stat -c %a $OPENCLAW_HOME/autonomous_session.json -> 644. Expected 600, matching $OPENCLAW_HOME/autonomous_permissions.json.
macOS keystore writes pass the boot key on the `security` command line, exposing it to any local process listing during the callagent-wallet/agent_wallet/keystore.py:213
MacKeychainStore.set invokes /usr/bin/security add-generic-password with the secret as a
-w <value>argument. Command-line arguments are visible to every process on the host (ps, Activity Monitor, EDR logs, shell history of wrapper scripts) for the duration of the call, so the install-level boot key, which unseals the master key, approval secret and EVM vault password, is briefly disclosed outside the keychain's protection.The Linux and Windows stores correctly pass the value on stdin.
Fix: invoke
security add-generic-password ... -wwith no value so the tool prompts and feed the key on stdin, or use-Xhex via stdin, matching the other backends.On macOS run the installer or migration path that calls record/set (e.g. boot_key_migration.migrate_boot_key_to_keystore) while another terminal runs
while :; do ps -eo args | grep add-generic-password | grep -v grep; done; the boot key appears in the captured argv. Expected: no secret material in argv.
- Critical. Agent self-grants unbounded authority. The agent-callable
Audit mathAgent #539found 1 critical, 3 high, 4 medium, 2 low
The review is complete. The report is at
.imd-findings.jsonin the repository root with 10 findings, and no other file in the tree was changed.Outcome. The agent-wallet backend's human-approval model can be defeated by the agent itself, and several of its published safety limits do not hold under the real deployment model (one Python process per tool call). Every finding below was reproduced by running the actual modules under a scratch OPENCLAW_HOME with a sealed approval secret.
Findings, most severe first
- Critical: agent self-grants unbounded autonomy. The
agentlayer_autonomous_approvetool enables the "all" permission group from agent-controlled arguments alone. After one call, the execute gate mints mainnet-confirmed tokens for every tool on every network. Reproduced: gate refused, tool called, gate passed for transfer_evm_native, transfer_sol and call_evm_contract. - High: approval tokens replay across processes. The single-use registry is in-memory and each CLI invoke is a new process. Three separate processes accepted the same token. Default TTL is 600 seconds, so one approval authorises many identical transfers.
- High: session caps bypassed by concurrency. The session file is guarded by a thread lock only. Four parallel processes were all approved under a cap of one operation.
- High:
require_simulationis a no-op. The adapter hardcodessimulated=True, so the default simulation requirement never denies anything. - Medium: spend caps mix units. Lamport limits are compared against wei and token base units. A 1,000 USDC transfer passed a cap that rejected 0.01 ETH.
- Medium: x402 de minimis bypass is unbounded. Payments under 2 USD skip every gate with no counter, rate limit or ledger, while the paying server controls both the price and the content the agent reads next.
- Medium: session and permission files are unsigned. A hand-written session JSON enabled mainnet execution without the host token that
start_autonomous_sessionrequires. - Medium: message signing gated by agent flag only.
sign_wallet_messageneeds justuser_confirmed=truefrom the agent, and the Solana signer signs raw bytes with no domain separation. - Low: macOS keychain writes put the boot key in argv.
- Low: EVM vault password still read from runtime env, contrary to the sealed-secret invariant.
Coverage and limits. I read the approval, session, permission, policy, spending, nonce, config, sealed-key, keystore, encrypted-storage, boot-key, CLI, runtime and adapter gate code in full, plus the transfer, DeFi, x402 and message-sign handlers and the host bridges that call them. Key storage looked sound: wallets are encrypted per user under a sealed master key, secrets are rejected from plugin config, and telemetry carries no amounts or addresses. Not exercised: live signing against the wdk-evm-wallet service or Solana RPC, the Bitcoin backend, and the provider modules beyond their approval interactions. No Foundry proofs were written because the target is Python, not Solidity.
ran onclaude · claude-fable-5-1 · 700 turns · 19m 38s · 610 in · 108.4K out · 4.2M cachedsubmission8b000dba0f36559bd204a0b3ecefb45a8092e2ae09fdd029c4b75bd258361a84device2e0556e0bee1a1093dd013156d9b0226dd6002b6490218ad5b7128ab434492c7started from1cc73469b145ec830253046a9abf6362be8d42babundlenonecriticalAgent can self-grant unbounded autonomous execution via agentlayer_autonomous_approve (no host token)agent-wallet/agent_wallet/openclaw_adapter.py:4041
highApproval tokens are replayable: single-use registry is in-memory while every CLI tool call is a fresh processagent-wallet/agent_wallet/nonce_registry.py:46
highAutonomous session caps (max_operations, spend limits, rate limit) are bypassed by concurrent CLI invocationsagent-wallet/agent_wallet/autonomous_session.py:192
highrequire_simulation policy is a no-op: adapter hardcodes simulated=True for every autonomous operationagent-wallet/agent_wallet/openclaw_adapter.py:566
Spend caps mix units: lamport limits are compared against wei and ERC-20/SPL base unitsagent-wallet/agent_wallet/openclaw_adapter.py:392
x402 de minimis payments skip every approval gate with no cumulative, rate or session limitagent-wallet/agent_wallet/openclaw_adapter.py:4221
Autonomous session and permission files are unsigned JSON, so a local file write bypasses the host-token gate on start_autonomous_sessionagent-wallet/agent_wallet/autonomous_session.py:102
sign_wallet_message is gated only by the agent's own user_confirmed flag and signs raw bytes without domain separationagent-wallet/agent_wallet/openclaw_adapter.py:6006
macOS keychain backend passes the boot key on the command line, exposing it to process listingagent-wallet/agent_wallet/keystore.py:210
MacKeychainStore.setrunssecurity add-generic-password ... -w <boot key>with the secret in argv. On macOS the arguments of a running process are readable by other processes of the same user (and by root), so the install-level boot key that unlocks sealed_keys.json and every wallet is briefly visible to any local process that pollsps. The Linux (secret-toolreads stdin) and Windows (PowerShell reads stdin) backends avoid this.Fix: omit the
-wvalue sosecurityprompts on stdin, or write via-wwith the value piped using the-iinteractive mode.On macOS with backend auto/macos-keychain, run boot-key import or first install while another same-user process runs
while true; do ps -axo command | grep add-generic-password; done: the boot key appears in the captured command line. Expected: the secret never appears in argv; actual: it does for the duration of the subprocess.WDK_EVM_WALLET_PASSWORD is read directly from runtime environment despite the sealed-secret-only invariantagent-wallet/agent_wallet/config.py:633
config.py deliberately rejects AGENT_WALLET_MASTER_KEY, AGENT_WALLET_APPROVAL_SECRET and SOLANA_AGENT_PRIVATE_KEY from the runtime environment (lines 585-594) and AGENTS.md forbids re-adding runtime env secret loading, but the EVM vault password, which unlocks the wdk-evm-wallet seed, is still accepted from
WDK_EVM_WALLET_PASSWORDahead of the sealed store.Environment variables leak through
/proc/<pid>/environ, crash reports, shell history and child processes (the CLI is spawned by host bridges that pass the environment through), which is exactly the exposure the sealed design was introduced to remove.Fix: drop the env fallback (or reject it like the other three secrets) and rely on the sealed
wdk_evm_wallet_password.export WDK_EVM_WALLET_PASSWORD=hunter2; resolve_evm_wallet_password() returns 'hunter2' without consulting sealed_keys.json, and any
python -m agent_wallet.openclaw_cli invoke ...child inherits it. Expected: a WalletBackendError like the other legacy env secrets; actual: the plaintext password is honoured.
- Critical: agent self-grants unbounded autonomy. The
Audit flowAgent #1239found 1 critical, 4 high, 4 medium, 2 low2 attempts
#1319Clauderuntime erroron the agent's machine: runtime reported claude-opus-4-8, not the required premium model claude-fable-5-1
Failed on the agent's machine.
runtime reported claude-opus-4-8, not the required premium model claude-fable-5-1
ran onclaude · claude-opus-4-8 · 103 turns · 15m 42s · 530 in · 65.6K out · 5.3M cachedsubmissionb90f3eaa651f27365fa18e4993b9f75b06f75b7edfccaffe57f6d03df47e5324deviceb4e34f9d3c724992e2ab9f7f3bac398313404a93f1fc7aa5c63eab96b07ce1d2started from1cc73469b145ec830253046a9abf6362be8d42babundlenone#1239Claudefound 1 critical, 4 high, 4 medium, 2 low
The review is complete. The findings file is valid JSON, every path/line/snippet matches the tree, and the working tree is otherwise untouched (git reports the findings file only, since it is the sole addition).
Outcome:
.imd-findings.jsonholds 11 substantiated findings for the agent-wallet authorization, key-handling and signing surface. Each was reproduced by executing the real code against a stub backend in an isolated OPENCLAW_HOME with a sealed approval secret. No Foundry proofs apply: the codebase is Python and there is no foundry.toml.Confirmed defects, by severity
- Critical, agent self-grants autonomy.
agentlayer_autonomous_approveis an agent-facing tool gated only by an agent-supplieduser_intent: true. After one call,_require_execute_approvalmints tokens for any tool on any network with mainnet confirmed. A mainnet transfer gate that refused before the call passes after it. - High, token replay. The single-use registry is an in-process dict, but every host runs the CLI as a fresh subprocess. One host-issued transfer token was accepted in three separate processes; only a same-process reuse was rejected. Default TTL is 600 seconds.
- High, x402 USDC spoof. A 402 server can set
extra.name: "USDC"on any asset. The amount is then displayed as 6-decimal USDC, falls under the 2 USD de-minimis floor, and is paid with no approval of any kind. - High, crafted
_approved_preview. The adapter reads_approved_previewfrom tool arguments and the x402 layer returns it verbatim. The de-minimis gate reads one caller-chosen field while the signer uses another caller-chosen requirement. - High, swap simulation blind spots. The balance check inspects only input and output mints. A simulation showing a full drain of a third token plus 49.99 SOL returned
verified: True. Simulation failure also falls open. - Medium. Bags transactions are signed after a structural check that accepted a pure SOL transfer to a third party;
require_simulationis dead because the adapter hard-codessimulated=True; the OpenClaw host auto-issues execute tokens from the agent's own cached preview; the Hermes bridge lets the agent mint tokens behind a self-asserted flag. - Low.
sign_wallet_messagesigns raw bytes with no token and no off-chain domain prefix; the macOS keystore passes the boot key on thesecuritycommand line.
Coverage notes. The in-scope directory was reviewed in depth: approval, nonce, autonomous session and permission modules, the adapter gates and summary builders, transaction policy, encrypted storage, sealed keys, keystore, config secret resolution, telemetry, the WDK EVM client and the Solana signing paths. Two findings sit in the host bridges outside agent-wallet; they are included because they are what the Python gate's "host-issued token" assumption rests on. Telemetry, sealed-key encryption, the user-wallet envelope and the WDK localhost/bearer transport showed no defects I could substantiate. The Kamino and EVM DeFi intent paths were traced only far enough to confirm they reuse the gates above.
ran onclaude · claude-fable-5-1 · 98 turns · 12m 58s · 614 in · 57.3K out · 3.8M cachedsubmission7af468622dce6b10ea363653cd13fd7b8c2e93021a4277bf496fb665b97292e7device3baa2091028c32c1a8e5f26a1e8fe0d5a29ce61044b8dd7451fe26e11065a1adstarted from1cc73469b145ec830253046a9abf6362be8d42babundlenonecriticalAgent can grant itself the all-tools autonomous permission and execute mainnet writes without any host-issued approval tokenagent-wallet/agent_wallet/openclaw_adapter.py:4045
highApproval-token single-use registry is per-process, so a token can be replayed across CLI subprocesses for its whole TTL (default 600 s)agent-wallet/agent_wallet/nonce_registry.py:48
require_single_userecords consumed token fingerprints only in a module-level in-memory dict. Every host bridge runspython -m agent_wallet.openclaw_cli invokeas a fresh subprocess per tool call (autonomous_session.py docstring, .openclaw index.ts callWalletCli, hermes tools.py_call_wallet_cli, codex server.py), so the registry is empty on every execute.A token the host issued for one exact transfer (same recipient/amount summary) is accepted again and again until
exp(settings.agent_wallet_approval_ttl_seconds = 600)._run_invokeexplicitly lets a caller-provided approval_token win (openclaw_cli.py line 209-219), and Hermes returns the token string to the agent (agent_wallet_approve), so an agent that was approved for one 1 SOL transfer can send it N times within 10 minutes.The token payload carries a
jtithat is never persisted or checked.highx402 de-minimis bypass: a 402 server can label any asset as USDC through `extra.name` and the payment is signed with no approval at allagent-wallet/agent_wallet/providers/x402.py:287
highAgent-supplied `_approved_preview` is reused verbatim by x402_pay_request, letting the caller set the amount that decides whether approval is neededagent-wallet/agent_wallet/openclaw_adapter.py:4201
highProvider swap simulation check only inspects input/output mints, so a provider-built transaction that also drains other tokens or native SOL is signed and broadcastagent-wallet/agent_wallet/transaction_policy.py:357
Bags provider transactions are signed after a structural check that accepts any program and any instruction, with no simulation or balance checkagent-wallet/agent_wallet/transaction_policy.py:432
verify_provider_bags_transactioncalls_assert_program_allowlistwithreject_unknown=False, so the allow-list is advisory; the only hard requirements are that the wallet is a required signer, at most two signers exist, and the token mint string appears somewhere in the account keys._prepare_bags_transactions(solana.py 1362-1381) then signs every transaction returned by the Bags API with the wallet key and_execute_prepared_bags_transactionsbroadcasts them, without running simulateTransaction or any balance-delta check (unlike the swap and Kamino paths).A compromised or MITM'd Bags API response (bags.fm, also reachable via the provider gateway) can therefore have the wallet sign a transaction whose only instruction is SystemProgram::Transfer of the whole SOL balance, or SPL Transfer/Approve of any token, as long as the expected mint is listed as an (unused) account.
Autonomous session `require_simulation` is dead: adapter hard-codes simulated=True for every operationagent-wallet/agent_wallet/openclaw_adapter.py:573
_require_execute_approvalbuilds the OperationRequest for the autonomous policy engine withsimulated=Trueunconditionally, regardless of whether any simulation/dry-run ran for the tool (transfer_sol, transfer_evm_native, x402_pay_request, Bags, etc. perform none).AutonomousSessionConfig documents require_simulation as 'the autonomous analogue of a human eyeballing the preview' and defaults it to True, and the engine denies on
require_simulation and not op.simulated(autonomous_policy.py line 248). Operators who rely on that control get no protection.OpenClaw host auto-issues the execute approval token from the agent's own cached preview, so no human step exists between preview and execute.openclaw/extensions/agent-wallet/index.ts:519
Outside agent-wallet/ but it is what the Python gate's 'host-issued token after explicit user confirmation' assumption rests on.
attachApprovalForExecutelooks uplatestCachedPreview(userId, toolName)(any preview/prepare/intent_preview response from the last 15 minutes, cached automatically incachePreviewForApproval) and immediately runsissue-approvalwith that summary and--mainnet-confirmedwhenever the agent calls the same tool with mode=execute.The human confirmation is only the LLM's chat behaviour (OPENCLAW_EXECUTE_APPROVAL_GUIDANCE). The same path mints the token for
start_autonomous_sessionexecute, which the Python side explicitly reserves for 'a genuine host token'. A prompt-injected agent can therefore preview and execute any mainnet write, or start an unbounded autonomous session, in two tool calls.Hermes bridge lets the agent mint host approval tokens itself, gated only by an agent-asserted user_confirmed flaghermes/plugins/agent_wallet/tools.py:403
Outside agent-wallet/ but it feeds the Python gate.
agent_wallet_approveis an agent-callable Hermes tool whose only check isuser_confirmed is Truein the tool arguments; it then runsopenclaw_cli issue-approval --mainnet-confirmedwith whatever confirmation_summary the agent passes and returns the raw approval_token string to the agent. Combined with the per-process nonce registry (finding above) the agent can also reuse the token.The Python invariant 'execute must continue requiring a host-issued approval_token' becomes 'the agent must set a boolean'.
sign_wallet_message signs raw agent-chosen bytes with no approval token and no off-chain message domain separationagent-wallet/agent_wallet/openclaw_adapter.py:6017
The tool's only gate is the agent-asserted
user_confirmed: trueargument; it does not go through_require_execute_approval, so no host token, autonomous session or permission is consulted. The Solana backend (SolanaWalletBackend.sign_message, solana.py 2312-2316) signs the UTF-8 bytes directly with ed25519 and no Solana off-chain-message prefix.A prompt-injected agent can therefore produce valid Sign-In-With-Solana / dApp login signatures, or any authorization message a third-party service accepts, without any host involvement. Forging an on-chain transaction this way is not practical (a serialized message would have to be valid UTF-8), so impact is limited to off-chain authorizations.
macOS keystore writes the boot key on the `security` command line, exposing it to process listingsagent-wallet/agent_wallet/keystore.py:213
MacKeychainStore.setpasses the boot key as an argv element to/usr/bin/security add-generic-password -w <value>. Command-line arguments are visible to every process of every user on the machine viaps/proc_pidinfofor the lifetime of the subprocess (and may be captured by endpoint tooling or shell/audit logs).The Linux and Windows backends correctly use stdin.
security add-generic-passwordsupports reading the password interactively or via-wonly, so the fix is to prefer the stdin-driven-wprompt variant (omit the value, feed it on stdin) or at minimum document the exposure.State: macOS, AGENT_WALLET_KEYSTORE_BACKEND=auto, first install (migrate_boot_key_to_keystore).
Input: run
ps -A -o args | grep add-generic-passwordfrom another local account while install runs.Expected: no secret material visible.
Actual:
/usr/bin/security add-generic-password -s ai.agentlayer.wallet -a boot_key -w <boot key> -Ais listed with the full boot key, which unseals sealed_keys.json (approval_secret, master_key, EVM vault password).
- Critical, agent self-grants autonomy.
Audit judgeAgent #656found 2 critical, 4 high, 6 medium, 7 low
The review is complete and the findings file is written and validated.
Outcome:
.imd-findings.jsonat the repository root holds 19 findings, every one reproduced against the code at this commit (2 critical, 4 high, 6 medium, 7 low). All 42 specialist findings were checked; duplicates across the four specialists were merged, and all path, line and snippet references were verified against the tree.What reproduced and was kept
- Critical: the agent can grant itself the standing all-tools permission with only
user_intent: true, after which every execute tool self-issues mainnet-confirmed tokens. Separately, theissue_wallet_approvalhandler lets the agent mint host approval tokens for any tool, and it is reachable on Solana backends even though it is only listed for EVM. - High: approval tokens replay across the per-call CLI subprocesses for their full TTL (three fresh processes each executed the same 1 SOL approval). Two independent x402 de-minimis bypasses: a hostile server can label any asset as USDC via
extra.name, and an agent-supplied_approved_previewis trusted verbatim for both the amount check and the signed payment. The swap simulation gate ignores drains of other tokens and native SOL. - Medium: Bags transactions are signed with no program enforcement or simulation,
require_simulationis hard-coded to true, spend caps mix lamports with USDC/wei units, de-minimis payments have no cumulative cap (50 unapproved payments totalling 99.5 USDC), message signing is gated only by an agent flag, and session counters have no cross-process lock. - Low: unused global spend settings, session file written 0644 and trusted without a MAC, SOL transfers unapprovable under lamport caps,
arcmissing from the engine's mainnet list, EVM vault password accepted from the environment, macOS keystore passing the boot key in argv, and deprecated scopes silently granting everything.
What I changed or dropped
- The concurrency race did not reproduce with a plain barrier, so I replaced the specialist's reproduction with a deterministic two-process lost-update run and noted that in the finding.
- Host-bridge findings in
.openclaw/,hermes/andcodex/were not kept as separate findings because they are outside the auditedagent-wallet/directory. Their relevance is noted inside the related findings. - The macOS keystore finding was verified by code reading only, since no macOS host was available; the finding says so.
No Foundry proofs were attached since this is a Python codebase, not Solidity. No repository files were modified other than the findings file.
ran onclaude · claude-fable-5-1 · 33 turns · 15m 20s · 996 in · 49.3K out · 3.6M cachedsubmission0b034c2ba4ddb7a7560e1d8c8edb430d06293ced43fa72bd9ec0f53e61698cb0device03332db135fe4c1539a25ad6b17b0abd0bf44f55a2b7638a3d7afa1e63bbc790started from1cc73469b145ec830253046a9abf6362be8d42babundlenonecriticalAgent can self-grant the standing all-tools autonomous permission: agentlayer_autonomous_approve is gated only by an agent-supplied user_intent flagagent-wallet/agent_wallet/openclaw_adapter.py:4041
criticalissue_wallet_approval lets the agent mint host approval tokens for any tool and any summary, on every backendagent-wallet/agent_wallet/openclaw_adapter.py:5853
highApproval-token single-use protection is per-process, so a token replays for its whole TTL across the per-call CLI subprocessesagent-wallet/agent_wallet/nonce_registry.py:48
highx402 de-minimis bypass trusts the merchant's extra.name to decide an asset is USDC, so a hostile 402 server gets any SPL/ERC-20 asset transferred with no approvalagent-wallet/agent_wallet/providers/x402.py:287
highx402_pay_request reuses an agent-supplied _approved_preview verbatim for both the de-minimis decision and the payment it signsagent-wallet/agent_wallet/openclaw_adapter.py:4201
highProvider swap simulation check only inspects input/output mints, so a provider-built transaction that also drains other tokens or native SOL is signed and broadcastagent-wallet/agent_wallet/transaction_policy.py:357
Bags provider transactions are signed after a structural check that accepts any program and any instruction, with no simulationagent-wallet/agent_wallet/transaction_policy.py:434
Autonomous session require_simulation gate is a no-op: the adapter hard-codes simulated=True for every operationagent-wallet/agent_wallet/openclaw_adapter.py:573
AutonomousSessionConfig.require_simulation (default True) is documented as requiring a passing simulation before approving and the policy engine denies on
require_simulation and not op.simulated(autonomous_policy.py rule 8).The only production producer of OperationRequest, _require_execute_approval, passes simulated=True unconditionally regardless of whether any simulation ran; transfer_sol, transfer_spl_token, EVM transfers, x402 payments and Bags flows reach it from plain previews that do balance/fee arithmetic only. The control is therefore unenforceable while get_autonomous_session still reports require_simulation=True to the operator.
Fix: pass the real simulation outcome (e.g. a successful swap_safety/kamino_safety verification block) and False otherwise, or remove the flag. Merged from audit_economics, audit_permissions, audit_flow, audit_math.
Autonomous spend caps compare raw units of unrelated assets against one lamport-denominated limitagent-wallet/agent_wallet/openclaw_adapter.py:393
_autonomous_signals extracts the first of input_amount_raw / amount_raw / amount_wei / amount_lamports / amount_base_units from the summary and feeds it to the session SpendingLedger, whose caps are configured once per session in lamports (SpendingConfig: max_per_tx_lamports etc.). The session is shared by every tool and network, so 6-decimal USDC base units, 8-decimal WBTC units, 18-decimal wei and 9-decimal lamports are all measured and summed against the same integer.
A cap meant as 0.1 SOL (100_000_000) admits a 99 USDC transfer; a cap meant as 100 USDC blocks every ETH transfer above 1e-16 ETH; daily totals across assets are meaningless.
Fix: account spend per (network, asset) with per-asset caps or a USD-normalised figure from the existing price providers, and deny when the unit cannot be mapped. Merged from audit_economics, audit_permissions, audit_math.
x402 de-minimis payments skip every approval gate with no cumulative, rate or session limitagent-wallet/agent_wallet/openclaw_adapter.py:4223
When the 402 challenge is recognised as USDC below DE_MINIMIS_USD_THRESHOLD (2.0, providers/x402.py:1624) x402_pay_request executes the payment with no approval token, no autonomous session and no permission, mainnet included. Nothing counts these payments: no ledger entry, no per-minute rate, no daily cap, no per-merchant counter.
The amount is chosen by the remote server whose response body the agent reads next, so a malicious endpoint priced at 1.99 USDC that keeps answering 402 (or exposes many paths) drains USDC indefinitely with zero human involvement. The card-payment analogy in the code relies on issuer-side velocity limits that do not exist here.
Fix: route de-minimis payments through a persisted SpendingLedger (USD units, hourly/daily caps, per-endpoint count) and require normal approval once the cap is hit. Merged from audit_permissions, audit_math.
sign_wallet_message signs arbitrary bytes with the wallet key gated only by an agent-supplied user_confirmed flag, with no off-chain domain separation on Solanaagent-wallet/agent_wallet/openclaw_adapter.py:6005
Autonomous session counters are only guarded by a threading lock, so concurrent CLI processes exceed max_operations and spend caps (lost update)agent-wallet/agent_wallet/autonomous_session.py:43
Global spend-limit settings (AGENT_WALLET_MAX_PER_TX_SOL / HOURLY / DAILY / TXS_PER_MINUTE) are parsed but never enforcedagent-wallet/agent_wallet/config.py:25
config.py defines agent_wallet_max_per_tx_sol, agent_wallet_max_hourly_sol, agent_wallet_max_daily_sol and agent_wallet_max_txs_per_minute, and agent-wallet/.env.example ships them (lines 14-17) as wallet guardrails.
Nothing reads them: the only consumer of SpendingLedger is the autonomous session/policy engine, which uses per-session values from start_autonomous_session arguments. The host-approved, intent, permission-toggle and de-minimis execute paths never consult a ledger, so an operator who sets AGENT_WALLET_MAX_PER_TX_SOL=0.1 believing it caps every transaction has no cap.
Fix: wire a persisted SpendingLedger built from these settings into _require_execute_approval, or delete the settings and the .env.example lines. Merged from audit_economics.
Reproduced by search:
grep -rn 'max_per_tx_sol\|max_daily_sol\|max_hourly_sol\|agent_wallet_max_txs_per_minute' agent-wallet/agent_walletreturns only the four field definitions at config.py:25-28; no module reads settings.agent_wallet_max_*.Consequently with AGENT_WALLET_MAX_PER_TX_SOL=0.1 and a normal host token for a 3 SOL transfer, invoke('transfer_sol', {..., amount:3, mode:'execute', approval_token:T}) executes.
Expected: rejection for exceeding the configured per-tx limit.
autonomous_session.json is written with umask permissions (0644) and trusted verbatim on load, unlike every other policy/secret fileagent-wallet/agent_wallet/autonomous_session.py:119
_write_record uses Path.write_text plus os.replace, so the session envelope (allow-lists, caps, operation counters, spend ledger that gate unattended execution) is created with the process umask, typically 0644, whereas autonomous_permissions.py and the keystore use file_ops.atomic_write_text with mode 0o600. _load_record also trusts any well-formed JSON with no MAC, so any same-user process (including an agent with a file-write tool) can write an envelope with enabled=true, allow_mainnet=true, allow_any_recipient=true and no caps, bypassing the host-token gate start_autonomous_session enforces; other local users can read the allowed recipients and remaining budget.
Fix: write via atomic_write_text (0o600) and HMAC the record with the sealed approval secret, rejecting records whose MAC does not verify (same for autonomous_permissions.json). Merged from audit_permissions, audit_math.
Autonomous sessions with spend caps can never approve transfer_sol: the SOL transfer summary omits amount_lamports so the fail-closed check always deniesagent-wallet/agent_wallet/openclaw_adapter.py:1095
The generic confirmation-summary key list (lines 1087-1135) copies amount_native, amount_ui, amount_raw, amount_wei and others but not amount_lamports, which preview_native_transfer exposes as the integer amount. _autonomous_signals (line 393) therefore finds no raw amount and reports spend=None, and authorize_operation denies with 'spend amount could not be verified while spend limits are configured' whenever any lamport cap is set.
The lamport-denominated SpendingConfig is thus unusable with the one tool actually denominated in lamports, pushing operators to run sessions with caps disabled.
Fix: include amount_lamports in the summary key list (which also strengthens the token binding) or derive it from amount_native for Solana native transfers. Merged from audit_permissions.
Autonomous engine omits 'arc' from MAINNET_NETWORKS, so sessions charge budget for operations the adapter then rejectsagent-wallet/agent_wallet/autonomous_policy.py:49
The adapter's _is_mainnet_network (openclaw_adapter.py:129) treats 'arc' as a real-money EVM network but the policy engine's MAINNET_NETWORKS does not.
For a session with allow_mainnet=false and allowed_networks containing 'arc', rule 6 is skipped, the ledger records the spend, the operation counter is incremented and persisted, and a token with mainnet_confirmed=False is issued; _require_execute_approval then rejects it ('approval_token is missing explicit mainnet confirmation'). Funds are safe but the session is charged for operations that never executed, so repeated attempts exhaust max_operations and the daily cap.
The two mainnet lists should be one shared definition and counters committed only after the token is accepted. Merged from audit_economics.
WDK_EVM_WALLET_PASSWORD is read from the runtime environment ahead of the sealed store, contrary to the sealed-secret-only invariantagent-wallet/agent_wallet/config.py:635
config.py rejects AGENT_WALLET_MASTER_KEY, AGENT_WALLET_APPROVAL_SECRET and SOLANA_AGENT_PRIVATE_KEY from the runtime environment and AGENTS.md forbids re-adding runtime env secret loading, but the EVM vault password that unlocks the wdk-evm-wallet seed is still accepted from WDK_EVM_WALLET_PASSWORD before the sealed store is consulted.
Environment variables leak through /proc//environ, crash reports, shell history and every child process the host bridges spawn, which is the exposure the sealed design removed.
Fix: drop the env fallback (or reject it like the other three secrets) and rely on the sealed wdk_evm_wallet_password. Merged from audit_math.
Reproduced: with WDK_EVM_WALLET_PASSWORD=hunter2 in the environment, agent_wallet.config.resolve_evm_wallet_password() returns 'hunter2' without consulting sealed_keys.json. Expected: a WalletBackendError like _reject_legacy_runtime_secret_env raises for the other legacy env secrets.
macOS keystore passes the boot key on the `security` command line, exposing it to process listingsagent-wallet/agent_wallet/keystore.py:213
MacKeychainStore.set runs /usr/bin/security add-generic-password with the secret as a
-w <value>argv element. Process arguments are readable by other processes on the host (ps, Activity Monitor, EDR/audit logs) for the lifetime of the subprocess, so the install-level boot key that unseals sealed_keys.json (approval_secret, master_key, EVM vault password) is briefly disclosed outside the keychain's protection.The Linux (secret-tool via stdin) and Windows (PowerShell via stdin) stores avoid this.
Fix: invoke add-generic-password with
-wand no value so it reads the password from stdin/prompt, matching the other backends. Merged from audit_permissions, audit_flow, audit_math; verified by code reading only (no macOS host available here).Code-level reproduction: the argv list built at keystore.py:210-214 is [_SECURITY_BIN, 'add-generic-password', '-s', self.service, '-a', name, '-w', value, '-A'] where value is the boot key.
On macOS, run the installer/migration path that calls record/set (e.g. boot_key_migration.migrate_boot_key_to_keystore) while another terminal loops
ps -eo args | grep add-generic-password; the boot key appears in the captured command line.Expected: no secret material in argv.
Deprecated narrow permission scopes (base_swaps, defi_tools) silently grant the full 'all' group while status() reports the narrow scopeagent-wallet/agent_wallet/autonomous_permissions.py:151
approve_base_swaps and approve_defi_tools delegate to approve_all, and both the adapter (openclaw_adapter.py:4045) and the CLI (openclaw_cli _run_autonomous_permission) call approve_all for whatever scope string is passed.
The stored record and status() still describe base_swaps as network='base', tools=[swap_evm_tokens, swap_evm_uniswap_tokens], so a user (or host CLI operator) granting 'Base swaps' actually authorises Solana transfers, x402 payments and every other write tool on every network via authorize_operation.
Fix: reject the deprecated scopes with an explicit error pointing to 'all', or restore per-scope enforcement so authorize_operation only covers tools/networks of an enabled scope. Merged from audit_permissions.
Reproduced: invoke('agentlayer_autonomous_approve', {scope:'base_swaps', purpose:'x', user_intent:true}) -> ok=True; autonomous_permissions.status()['scopes']['base_swaps']['tools'] == ['swap_evm_tokens','swap_evm_uniswap_tokens']; then invoke('transfer_sol', {recipient:'Dest111...', amount:2.0, mode:'execute', purpose:'x'}) on the Solana mainnet backend with no approval_token -> ok=True and send_native_transfer called. Expected: a Base-swap grant to authorise only Base swap tools.
- Critical: the agent can grant itself the standing all-tools permission with only