# Audit report

> 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.

| | |
|---|---|
| Repository | https://github.com/lopushok9/Agent-Layer.git |
| Commit | `1cc73469b145ec830253046a9abf6362be8d42ba` |
| Job | `a99dc20d-ebea-4abf-824d-ce94ceb1bc65` |
| Judged | 2026-10-12 06:39 UTC |
| Findings | 2 critical · 4 high · 6 medium · 7 low |

Four agents audited the code as it is at `1cc7346`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Critical: Agent can self-grant the standing all-tools autonomous permission: agentlayer_autonomous_approve is gated only by an agent-supplied user_intent flag

`agent-wallet/agent_wallet/openclaw_adapter.py:4041`

```
                if args.get("user_intent") is not True:
```

agentlayer_autonomous_approve is an ordinary agent-callable tool (registered through _autonomous_permission_tool_specs and returned by list_tools on every backend). Its only guards are a scope in SUPPORTED_SCOPES, a non-empty purpose string and args['user_intent'] is True, all three supplied by the model. On success it calls autonomous_permissions.approve_all() (line 4045), which persists an enabled 'all' group to OPENCLAW_HOME/autonomous_permissions.json with no tool, network, recipient, spend or TTL bound. From then on _require_execute_approval (lines 585-590) mints its own approval token via autonomous_permissions.authorize_operation() for ANY write tool on ANY network with mainnet_confirmed=True (autonomous_permissions.py authorize_operation documents 'no per-tool or per-network allow-list'). This defeats the AGENTS.md invariant that execute 'must continue requiring a host-issued approval_token'. The sibling tool start_autonomous_session (lines 4087-4103) correctly demands a genuine host token because 'an agent must not be able to widen its own envelope'; the strictly more powerful standing grant has no such check. The deprecated scopes base_swaps/defi_tools behave identically (approve_base_swaps -> approve_all), so a 'Base swaps only' request also unlocks Solana transfers and x402 payments. A prompt-injected or misbehaving agent therefore converts a per-transaction human-approval wallet into an unattended one with one tool call, persisted across sessions and hosts. Fix: require a host-issued, single-use approval token bound to {operation:'agentlayer_autonomous_approve', scope} exactly as start_autonomous_session does (or remove the agent-facing tool and keep the grant only on the host CLI path), and never treat user_intent as the authorization signal. Merged from audit_economics, audit_permissions, audit_flow, audit_math.

**Reproduction**

Reproduced with OpenClawWalletAdapter over the smoke-test FakeBackend (chain solana, network mainnet), sealed approval_secret installed, no session, no permission file. Step 1: invoke('transfer_sol', {recipient:'Dest111...', amount:1.0, mode:'execute', purpose:'x'}) with no approval_token -> ok=False, error 'SOL transfer execution requires a host-issued approval_token ...'. Step 2: invoke('agentlayer_autonomous_approve', {scope:'all', purpose:'speed', user_intent:true}) -> ok=True, data.active=True; autonomous_permissions.json written. Step 3: invoke('transfer_sol', {recipient:'Dest111...', amount:5.0, mode:'execute', purpose:'x'}) still with no approval_token -> ok=True and the backend's send_native_transfer was called with 5.0 SOL on mainnet. Variant: step 2 with scope:'base_swaps' -> status() lists tools ['swap_evm_tokens','swap_evm_uniswap_tokens'] yet step 3 (transfer_sol, 2.0 SOL) also succeeds. Expected: step 2 rejected without a host-issued token, so step 3 keeps failing.

### 2. Critical: issue_wallet_approval lets the agent mint host approval tokens for any tool and any summary, on every backend

`agent-wallet/agent_wallet/openclaw_adapter.py:5853`

```
            if tool_name == "issue_wallet_approval":
```

The invoke() dispatcher handles issue_wallet_approval for any backend (the tool spec is only inserted into the EVM tool list, but _invoke_tool_dispatch never checks list_tools membership, so it is reachable on Solana backends too). The handler takes tool_name, summary (any object) and mainnet_confirmed straight from the agent's arguments and calls approval.issue_approval_token() with the sealed approval_secret (line 5873-5878, issued_by='claude-code-bridge'), returning the signed token to the caller. No host involvement, no user_intent check (the spec sets requires_explicit_user_intent=True but the handler never reads it), no check that the summary came from a real preview. Because every execute path only verifies the HMAC and the (tool, network, summary) binding, an agent can approve its own transfers, swaps, DeFi operations, x402 payments and even start_autonomous_session envelopes. The handler is reachable from every host that forwards tool calls verbatim (openclaw_cli invoke, the Hermes bridge, the Codex/Claude Code MCP server). Fix: remove the tool from the agent-facing dispatcher and issue tokens only from the host process (openclaw_cli issue-approval / OpenClawWalletRuntimeContext.issue_execute_approval) after an out-of-band user confirmation; if a bridge must keep it, gate it behind a host-held secret or interactive confirmation rather than agent arguments. Merged from audit_economics.

**Reproduction**

Reproduced with OpenClawWalletAdapter over the smoke-test FakeBackend (solana mainnet), sealed approval_secret present, no session/permission. list_tools() does NOT contain issue_wallet_approval on this backend, yet: Step 1: invoke('transfer_sol', {recipient:'Dest111...', amount:1.0, mode:'prepare', purpose:'x', user_intent:true}) -> copy data.confirmation_summary S. Step 2: invoke('issue_wallet_approval', {tool_name:'transfer_sol', summary:S, mainnet_confirmed:true}) -> ok=True, data.approval_token=T, usage 'Pass approval_token to transfer_sol with mode=execute'. Step 3: invoke('transfer_sol', {recipient:'Dest111...', amount:1.0, mode:'execute', purpose:'x', approval_token:T}) -> ok=True, send_native_transfer called. Expected: only a host-issued token after human confirmation can satisfy execute; the agent should not be able to obtain T.

### 3. High: Approval-token single-use protection is per-process, so a token replays for its whole TTL across the per-call CLI subprocesses

`agent-wallet/agent_wallet/nonce_registry.py:48`

```
_registry = NonceRegistry()
```

require_single_use() records consumed token fingerprints in a module-level in-memory dict. The documented deployment runs `python -m agent_wallet.openclaw_cli invoke` as a fresh subprocess per tool call (autonomous_session.py docstring; the OpenClaw extension, Hermes plugin and Codex bridge all spawn it per call), so the registry is empty at the start of every execute and verify_approval_token never checks the token's jti against any persisted store. A host token for one exact transfer therefore authorises that transfer repeatedly until exp (settings.agent_wallet_approval_ttl_seconds defaults to 600). The native-transfer binding summary carries no per-execution nonce (only operation, network, from_address, to_address, amount_native), so the same token verifies again each time. Tokens are held by the agent in the Hermes flow and via issue_wallet_approval, and appear in CLI argv in all hosts. Replaying the start_autonomous_session token also re-runs start_session(), resetting operations and spend_entries of an existing session. Fix: persist consumed jti values (with exp) under OPENCLAW_HOME behind a file lock, check them in the same critical section as verify_approval_token, or bind each execute token to a one-shot value the backend consumes. Merged from audit_economics, audit_permissions, audit_flow, audit_math.

**Reproduction**

Reproduced. Issue one token T = issue_approval_token(tool_name='transfer_sol', network='mainnet', summary=<_build_confirmation_summary of the 1 SOL preview: keys amount_native, from_address, network, operation, to_address>, mainnet_confirmed=True). In one process: invoke('transfer_sol', {..., mode:'execute', approval_token:T}) #1 -> ok=True, #2 -> ok=False 'Approval token has already been used'. Then spawn three separate Python processes that each build the adapter and call the same execute with the same T: all three print ok=True and each calls send_native_transfer (3 SOL moved for one 1 SOL approval). Expected: any use after the first, in any process, rejected.

### 4. High: x402 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 approval

`agent-wallet/agent_wallet/providers/x402.py:287`

```
    return _trim(extra.get("name")).lower() in _USDC_IDENTIFIERS
```

x402_pay_request skips _require_execute_approval entirely when is_de_minimis_payment(preview) is true (openclaw_adapter.py 4221-4235). That decision reads x402_amount_display, which normalize_payment_requirement only fills when _looks_like_usdc() is true, and _looks_like_usdc accepts either a known USDC asset id OR any requirement whose server-supplied extra.name is 'usdc'/'usd coin', without cross-checking the asset field. The payment itself is built by the x402 SDK from the requirement's real asset and amount. A malicious endpoint can therefore return asset=<any mint/contract the wallet holds>, amount=1999999, extra.name='USDC' and the wallet signs and submits 1,999,999 raw units of that asset to payTo with no host token, session, permission or user confirmation (for 8-decimal cbBTC that is ~0.02 BTC; for low-decimal tokens far more). The agent only has to be induced to call x402_pay_request on an attacker URL. Fix: treat a requirement as USDC only when asset itself is in the vetted per-network USDC list, ignore extra.name for this purpose, and apply de minimis only when decimals are known to be 6. Merged from audit_permissions, audit_flow.

**Reproduction**

Reproduced: req={'scheme':'exact','network':'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp','asset':'cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij','amount':'1999999','payTo':'Attacker111...','extra':{'name':'USDC'}}; n = x402.normalize_payment_requirement(req, source='t') -> n['amount_display'] == '1.999999'; x402.is_de_minimis_payment({'payment_required':True,'x402_amount_display':n['amount_display']}) -> True, so openclaw_adapter.py:4224 never calls _require_execute_approval and pay_and_fetch signs the cbBTC transfer. The same requirement without extra.name yields amount_display None (approval required). Expected: amount_display None and de_minimis False whenever asset is not a known USDC id.

### 5. High: x402_pay_request reuses an agent-supplied _approved_preview verbatim for both the de-minimis decision and the payment it signs

`agent-wallet/agent_wallet/openclaw_adapter.py:4201`

```
                approved_preview = args.get("_approved_preview")
```

x402_pay_request reads _approved_preview from the tool arguments and passes it to x402.resolve_payment_preview(). _reusable_approved_preview (x402.py 1592-1615) returns the dict unchanged whenever request_fingerprint matches the (computable) SHA over method/URL/body and it carries a selected_payment plus a payment-required header; no fresh probe is made and nothing about the object is authenticated. The adapter then derives the approval decision from that same object: is_de_minimis_payment reads preview['x402_amount_display'] (line 4223) and skips _require_execute_approval when it parses below $2, while pay_and_fetch signs from preview['selected_payment'] and preview['response_headers']['payment-required'], i.e. the real (possibly $50 or attacker-chosen) requirement. The gate reads one field and the signer reads another, both caller-controlled. _approved_preview is agent-controlled on every host that forwards arguments verbatim (openclaw_cli invoke, Hermes), and the OpenClaw/Codex bridges only fill it when absent. Fix: never derive the approval decision from caller-supplied data (compute de minimis from a fresh probe or re-normalised selected_payment), require _approved_preview to carry a host-bound digest before reuse, and ignore it when no approval_token is present. Merged from audit_economics, audit_flow.

**Reproduction**

Reproduced: url='https://pay.example.test/report'; fp=x402._build_request_metadata(url=url, method='GET')['request_fingerprint']; crafted={'payment_required':True,'request_fingerprint':fp,'selected_payment':{'scheme':'exact','network':'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp','asset':'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v','amount':'250000000000','pay_to':'Attacker111...'},'x402_amount_display':'0.01','response_headers':{'payment-required':'ZZZ'}}; resolved = await x402.resolve_payment_preview(backend=FakeBackend(), url=url, method='GET', approved_preview=crafted) -> resolved == crafted (returned verbatim, no network call) and x402.is_de_minimis_payment(resolved) -> True. In the adapter this means invoke('x402_pay_request', {url, purpose, _approved_preview: crafted}) with no approval_token skips _require_execute_approval and pay_and_fetch proceeds to sign the 250,000 USDC payment described by the crafted selected_payment/header. Expected: a fresh probe (or a host-authenticated preview) decides the amount; a $0.01 display string must not authorise a $250,000 payment.

### 6. High: Provider swap simulation check only inspects input/output mints, so a provider-built transaction that also drains other tokens or native SOL is signed and broadcast

`agent-wallet/agent_wallet/transaction_policy.py:357`

```
        input_delta = token_deltas.get(input_mint)
```

verify_provider_swap_transaction deliberately accepts unknown program ids (reject_unknown=False at line 196) on the stated basis that the simulation check is the stronger swap-specific gate. But verify_provider_swap_simulation_result only bounds the delta of input_mint and output_mint, and checks the native lamport delta only when input_mint is wrapped SOL. Any other wallet token delta and, for token-input swaps, the native SOL delta are computed (token_deltas / native_delta_lamports) but never enforced. A compromised or spoofed swap provider response (the Jupiter/Ultra base URLs and provider gateway are configurable, and prepare_swap_from_preview trusts preview['quote_response']['transaction']) can append SPL transfer/approve or System transfer instructions and the wallet still signs (wallet_layer/solana.py ~5162) and broadcasts. If simulateTransaction raises ProviderError the code proceeds with no balance check at all (solana.py 5196-5206: 'Proceeding with structural provider verification to preserve swap availability'). Fix: reject any negative delta for mints other than input_mint, bound the native delta to a fee allowance when the input is not SOL, and fail closed when the simulation is unavailable. Merged from audit_flow.

**Reproduction**

Reproduced: verify_provider_swap_simulation_result(sim, wallet_address=W, wallet_account_index=0, input_mint=USDC, output_mint=JUP, input_amount_raw=10_000_000, minimum_output_amount_raw=100_000_000) with sim={'err':None,'preBalances':[50_000_000_000,0],'postBalances':[10_000_000,0],'preTokenBalances':[USDC 10_000_000, JUP 0, BONK 5_000_000_000_000 all owner=W],'postTokenBalances':[USDC 0, JUP 100_000_000, BONK 0]} returns verified=True with enforced_checks ['simulation_err_is_none','token_input_spend_within_approved_amount','token_output_meets_approved_minimum'] while its own token_deltas show BONK -5000000000000 and native_delta_lamports -49990000000. Expected: rejection, since the wallet loses 49.99 SOL and all BONK beyond the approved 10 USDC.

### 7. Medium: Bags provider transactions are signed after a structural check that accepts any program and any instruction, with no simulation

`agent-wallet/agent_wallet/transaction_policy.py:434`

```
        reject_unknown=False,
```

verify_provider_bags_transaction calls _assert_program_allowlist with reject_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 (wallet_layer/solana.py 1362-1381) then signs every transaction returned by the Bags API and _execute_prepared_bags_transactions broadcasts them, without simulateTransaction or any balance-delta check (unlike the swap and Kamino paths). A compromised or MITM'd Bags API response can have the wallet sign a transaction whose only effect 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. Fix: require at least one recognised Bags/DBC/Meteora program id, reject unknown programs, and simulate with the swap-style balance-delta verification before signing. Merged from audit_flow.

**Reproduction**

Reproduced with solders: wallet, attacker, mint = Keypair()x3; ix = system_program.transfer(from=wallet, to=attacker, lamports=50_000_000_000); ref = Instruction(SystemProgram, <any data>, [AccountMeta(wallet, signer), AccountMeta(mint.pubkey(), writable)]); msg = Message.new_with_blockhash([ix, ref], wallet.pubkey(), Hash.default()); verify_provider_bags_transaction(msg, wallet_address=str(wallet.pubkey()), token_mint=str(mint.pubkey()), action='Bags claim') -> returns verified=True, program_ids=['11111111111111111111111111111111', ...], unknown_program_ids=[]. Expected: rejected (no Bags/Meteora program, pure 50 SOL transfer to a third party).

### 8. Medium: Autonomous session require_simulation gate is a no-op: the adapter hard-codes simulated=True for every operation

`agent-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.

**Reproduction**

Reproduced: autonomous_session.start_session(AutonomousSessionConfig(enabled=True, allowed_tools={'transfer_sol'}, allowed_networks={'mainnet'}, allow_mainnet=True, allow_any_recipient=True, require_simulation=True)); invoke('transfer_sol', {recipient:'Dest111...', amount:1.0, mode:'execute', purpose:'x'}) with no approval_token over FakeBackend whose preview_native_transfer performs no simulation -> ok=True, send_native_transfer called, session operations=1. Control: AutonomousPolicyEngine.evaluate with simulated=False on the same config -> approved=False, rule='simulation'. Expected: the execute denied with 'operation has no passing simulation and require_simulation is true'.

### 9. Medium: Autonomous spend caps compare raw units of unrelated assets against one lamport-denominated limit

`agent-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.

**Reproduction**

Reproduced: start_session(AutonomousSessionConfig(enabled=True, allowed_tools={'transfer_spl_token'}, allowed_networks={'mainnet'}, allow_mainnet=True, allow_any_recipient=True, require_simulation=False, spending=SpendingConfig(max_per_tx_lamports=100_000_000, max_daily_lamports=200_000_000))) intending a 0.1 SOL / 0.2 SOL budget. invoke('transfer_spl_token', {recipient:'Dest111...', mint:'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', amount:99.0, decimals:6, mode:'execute', purpose:'x'}) with no approval_token (preview amount_raw=99_000_000) -> #1 ok=True, #2 ok=True (198 USDC sent), #3 denied by the daily cap. Expected: a 99 USDC transfer denied under a 0.1 SOL (~$20) per-tx cap.

### 10. Medium: x402 de-minimis payments skip every approval gate with no cumulative, rate or session limit

`agent-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.

**Reproduction**

Reproduced with x402.resolve_payment_preview patched to return a genuine-shape preview for a 1.99 USDC Solana requirement (asset EPjFWdd5..., amount 1990000, x402_amount_display '1.99') and x402.pay_and_fetch patched to record the amount: with autonomous_session.is_active()=False and autonomous_permissions.is_autonomous_approved()=False, 50 consecutive invoke('x402_pay_request', {url:'https://merchant.example/item/<i>', purpose:'x'}) calls with no approval_token all return ok=True; total recorded payment 99.5 USDC. Expected: an aggregate limit to stop the loop or force an approval.

### 11. Medium: 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 Solana

`agent-wallet/agent_wallet/openclaw_adapter.py:6005`

```
            if tool_name == "sign_wallet_message":
```

sign_wallet_message is the only signing tool outside the preview/prepare/execute + approval_token flow. Its gate is args.get('user_confirmed') is True, a boolean the model writes itself; no approval token, autonomous session or permission is consulted, so neither the host nor the autonomous framework can restrict or audit signatures. On Solana the backend (wallet_layer/solana.py:2312-2316) UTF-8 encodes the string and signs it raw with the funds-holding Ed25519 key with no off-chain-message envelope; on EVM it is forwarded to the message-sign endpoint. A prompt-injected agent can thus produce valid Sign-In-With-Solana/Ethereum logins, off-chain order or permit-style authorisations, or any valid-UTF-8 payload a verifier accepts, silently. AGENTS.md lists user_confirmed among the mandatory safety gates, but an agent-controlled boolean is not a gate. Fix: route message signing through the same host approval (token bound to {tool:'sign_wallet_message', message_sha256, purpose}), and on Solana sign with the off-chain message format (or reject inputs that parse as a transaction message). Merged from audit_economics, audit_permissions, audit_flow, audit_math.

**Reproduction**

Reproduced: with no session, no permission and no approval_token, invoke('sign_wallet_message', {message:'api.example wants you to sign in\nNonce: 1', purpose:'login', user_confirmed:true}) over the adapter -> ok=True and a signature is returned immediately. SolanaWalletBackend.sign_message source confirms `payload = message.encode('utf-8') ...; return b58encode(self.signer.sign_message(payload))` with no prefix. Expected: refusal until a host-issued approval bound to the message hash is supplied, and a domain-separated signing format.

### 12. Medium: 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()
```

authorize_operation guards its read-modify-write of autonomous_session.json with a threading.Lock, which only serialises threads inside one process. The module exists precisely because each tool call is a separate subprocess, yet there is no file lock (fcntl/msvcrt) and no compare-and-swap on the record. Two CLI processes whose load->evaluate->write windows overlap both pass the operations/ledger checks, both receive signed approval tokens, and the last writer wins, so persisted counters undercount; max_operations, max_txs_per_minute and hourly/daily caps can be exceeded by the concurrency factor. Hosts that run tool calls in parallel, or an agent that fans out calls, hit this window. Fix: take an exclusive advisory file lock around load->evaluate->write, or write with O_EXCL/rename-and-verify retry. Merged from audit_math; the specialist's barrier-based run was not reproducible here in 6-way parallel attempts (the window is a few ms), so the reproduction below widens the window deterministically.

**Reproduction**

Reproduced deterministically: start_session(AutonomousSessionConfig(enabled=True, allowed_tools={'transfer_sol'}, allowed_networks={'mainnet'}, allow_mainnet=True, allow_any_recipient=True, require_simulation=False, max_operations=1)). Process A patches autonomous_session._load_record to sleep 1 s after loading (standing in for scheduler delay between the read and the write inside authorize_operation), then calls authorize_operation(OperationRequest(tool_name='transfer_sol', network='mainnet', summary={...}, spend_amount=1, recipient='r', simulated=True)); process B starts 0.5 s later and calls the same without delay. Output: 'fast APPROVED', 'slow APPROVED', 'ops recorded after both: 1 (max_operations=1)'. Expected: exactly one approval and one denial 'operation budget exhausted', with the counter reflecting every approval.

### 13. Low: Global spend-limit settings (AGENT_WALLET_MAX_PER_TX_SOL / HOURLY / DAILY / TXS_PER_MINUTE) are parsed but never enforced

`agent-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.

**Reproduction**

Reproduced by search: `grep -rn 'max_per_tx_sol\|max_daily_sol\|max_hourly_sol\|agent_wallet_max_txs_per_minute' agent-wallet/agent_wallet` returns 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. Low: autonomous_session.json is written with umask permissions (0644) and trusted verbatim on load, unlike every other policy/secret file

`agent-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.

**Reproduction**

Reproduced: with umask 022, autonomous_session.start_session(AutonomousSessionConfig(enabled=True)) -> stat mode of $OPENCLAW_HOME/autonomous_session.json is 644 while autonomous_permissions.approve_all() produces autonomous_permissions.json at 600. Separately, writing $OPENCLAW_HOME/autonomous_session.json = {version:1, started_at:now, operations:0, spend_entries:[], config:{enabled:true, allowed_tools:['transfer_sol'], allowed_networks:['mainnet'], allow_mainnet:true, allow_any_recipient:true, require_simulation:false, spending:{}, max_operations:0, session_ttl_seconds:0, approval_ttl_seconds:120}} with no start_session call makes autonomous_session.is_active() True and invoke('transfer_sol', {..., amount:3.0, mode:'execute'}) with no approval_token succeeds (send_native_transfer called). Expected: 0600 and an envelope never authorised by a host token ignored.

### 15. Low: Autonomous sessions with spend caps can never approve transfer_sol: the SOL transfer summary omits amount_lamports so the fail-closed check always denies

`agent-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.

**Reproduction**

Reproduced: start_session(AutonomousSessionConfig(enabled=True, allowed_tools={'transfer_sol'}, allowed_networks={'mainnet'}, allow_mainnet=True, allow_any_recipient=True, require_simulation=False, spending=SpendingConfig(max_per_tx_lamports=5_000_000_000))); invoke('transfer_sol', {recipient:'Dest111...', 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 approved.

### 16. Low: Autonomous engine omits 'arc' from MAINNET_NETWORKS, so sessions charge budget for operations the adapter then rejects

`agent-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.

**Reproduction**

Reproduced: FakeBackend subclass with chain='evm', network='arc'; start_session(AutonomousSessionConfig(enabled=True, allowed_tools={'transfer_evm_native'}, allowed_networks={'arc'}, allow_mainnet=False, allow_any_recipient=True, require_simulation=False, max_operations=3)); call adapter._require_execute_approval(approval_token=None, tool_name='transfer_evm_native', summary={'operation':'EVM native transfer','network':'arc','recipient':'0xdead','amount_wei':'1'}, action_label='EVM native transfer', backend=be) four times -> errors ['approval_token is missing explicit mainnet confirmation.' x3, 'autonomous policy denied operation: autonomous operation budget exhausted (3 ops)'] with session operations=3. Expected: each attempt denied at rule 6 with no state change.

### 17. Low: WDK_EVM_WALLET_PASSWORD is read from the runtime environment ahead of the sealed store, contrary to the sealed-secret-only invariant

`agent-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/<pid>/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.

**Reproduction**

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. Low: macOS keystore passes the boot key on the `security` command line, exposing it to process listings

`agent-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 `-w` and 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).

**Reproduction**

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. Low: Deprecated narrow permission scopes (base_swaps, defi_tools) silently grant the full 'all' group while status() reports the narrow scope

`agent-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.

**Reproduction**

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.

---

Judge's submission `0b034c2ba4ddb7a7560e1d8c8edb430d06293ced43fa72bd9ec0f53e61698cb0`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
