Skip to main content
Glama
axdvdv

wwall

by axdvdv

wwall

一个位于 AI 代理与钱包之间的策略守卫。

Aleph 黑客松 2026 · WDK 赛道 · Tether

给代理一个钱包,就等于把你的钱交给了它。wwall 是一个 MCP 服务器,它挡在中间:代理可以提议一笔支付,但由本地的人工策略来决定是放行、需要人先批准、还是直接拒绝。每一条决策都会写入一个带签名的追加式账本。

代理永远不会直接接触 @tetherto/wdk。它接触的是 wwall,而 wwall 接触 WDK。

┌─────────────┐   MCP/stdio   ┌──────────────────────────┐        ┌──────────┐
│  AI agent   │──────────────▶│  wwall                   │───────▶│   WDK    │──▶ Polygon
│ (Claude…)   │  propose_     │  ├ predicates.ts (guard) │ only   │  wallet  │
│             │   payment     │  ├ ledger.jsonl (signed) │ if     └──────────┘
└─────────────┘◀──────────────│  └ policy.json           │ allowed
                 verdict      └──────────────────────────┘
                                          │ holds NEEDS_CONFIRM
                                          ▼
                                   ┌─────────────┐
                                   │   human     │  wwall pending / confirm / reject
                                   └─────────────┘

唯一不变量

任何工具调用在通过 evaluatePredicates() 之前,都无法触达 wallet.send()

其他所有设计都是为了确保这一点:

  • 服务器只注册了三个工具——propose_paymentget_balanceget_pending。没有发送、转账、签名或原始交易工具,并且有一个测试按名称和模式断言工具列表。

  • 钱包是惰性打开的,只在已经放行的路径上才会打开。被拒绝的提案根本不会构造 WDK 实例——回答它所需的代币配置被单独传入,正是出于这个原因。

  • 支付代币由配置固定。代理指定其他代币会在任何规则运行之前被拒绝。(否则,SPEND_CAP{token:"USDT0"} 就不会适用于 token:"MONOPOLY",策略会放行,而钱包仍然会转移真实的 USD₮0。支出上限不能用一个字符串就绕过。)

  • 拒绝是正常的工具结果,而不是抛出的错误,这样代理就能读取原因并向人解释。

  • 相同的谓词也注册在 WDK 内部,所以钱包本身也会拒绝——见下文。

两层,同一组谓词

wwall 决定是否调用 transfer。这是关于本进程的论证。WDK 自己的策略引擎决定钱包是否会执行它,这是账户的一个属性:

wdk.registerPolicy({
  id: 'wwall-guard', scope: 'project', wallet: 'polygon',
  rules: [{ operation: 'transfer', action: 'ALLOW',
            conditions: [ctx => evaluate(policy, intentFrom(ctx.args), context(), 'ignore-confirm')] }]
})

一条 ALLOW 规则,由其他所有东西使用的同一个 evaluatePredicates 门控。杠杆在于没有写什么:WDK 对受管账户是默认拒绝的,所以一旦有任何策略生效,OPERATIONS 中的每个方法都会被包装,任何没有匹配 ALLOW 的调用都会抛出 PolicyViolationError。这封堵了完全不经过 transfer 的支出上限绕过路径:

路由

为什么 transfer 形状的守卫拦不住

sendTransaction({to: token, data: <ERC-20 transfer calldata>})

效果相同,方法不同

approve(spender, MAX) 然后由他人的 transferFrom

资金稍后由他人之手转出

对 EIP-2612 Permit 进行 signTypedData

完全在链下;没有"发送"动作

ERC-7702 下的 delegate(...)

将账户交给合约

wwall 不会做这些事。这正是重点:这些都是它的守卫看不到的东西。test/wdk-policy.test.ts 驱动一个真实的 WDK 实例连接一个死 RPC,并断言每一项都会在任何网络调用之前抛出 PolicyViolationError——而允许的转账则会在网络上失败,这恰恰证明了它通过了守卫而不是被守卫拦下。

条件询问的是"这到底允不允许"(ignore-confirm 模式)。已执行与待确认的区分属于上一层:当一笔待确认的支付到达 transfer 时,已经有人批准了,这一层不能再次拒绝它。

在 ALLOW 规则上抛异常的条件被视为"不匹配",在默认拒绝下意味着被拒绝——所以这一层的 bug 会以失败安全的方式关闭。

从干净克隆快速开始

git clone <this repo> && cd wwall
npm install
cp .env.example .env        # then edit it — see below
npm run build
npm test                    # 295 tests, no network, no money
npm run try                 # walk the guard through a dozen proposals, fake wallet

.env 只需要一行。其他所有内容都有默认值:

WARDEN_SEED="…twelve words…"
# WARDEN_ARMED=1            # leave unset until you mean to move real funds

链、RPC 和支付代币默认为 Polygon 和 USD₮0;策略、账本和审计密钥位于 ~/.wwall/。有关每个变量及其覆盖作用,请参阅 .env.example

在不花费任何资金的情况下检查钱包:

npm run check:wallet

这会直接从代币合约读取 symbol()decimals(),并与你的配置进行比较。decimals 错误不是一条错误消息,而是相差 10ⁿ 倍的支付金额。

安全保险

WARDEN_ARMED 默认关闭。只有在其恰好为 1 时,策略允许的支付才会被报告但不会发送——提案会以 code: "not_armed"rejected 状态返回。请谨慎地打开它。

通过桌面扩展安装(推荐)

wwall 以 MCP Bundle 的形式发布——一个 .mcpb 文件,Claude Desktop 一键安装,设置通过表单而不是手写 JSON。

npm install && npm run build
npm run bundle          # → build/wwall.mcpb

然后在 Claude Desktop 中:设置 → 扩展 → 安装扩展… 并选择 build/wwall.mcpb

表单只要求一件事:助记词。 它在清单中声明为 "sensitive": true,因此 Claude Desktop 会将其掩码并保存在操作系统钥匙串中,而不是保存在你之后可能粘贴到 bug 报告中的配置文件里。

其他所有内容都有默认值,不会询问:

设置

默认值

链和 RPC

Polygon,通过公共端点

支付代币

USD₮0 — 0xc2132D05…,6 位小数,链上已验证

策略、账本、审计密钥

~/.wwall/

~/.wwall 特意不是工作目录:扩展以不可预测的 cwd 启动,因此相对于 cwd 的账本会让 CLI 和 MCP 服务器看到不同的消费历史——而基于错误账本计算的每日上限根本不算上限。现在两半读取的是同一个账本,所以从任何目录运行 wwall pending 都能看到扩展写入的内容。

首次运行时,wwall 会写入一个空白名单的起始 ~/.wwall/policy.json,因此在你指定收款人之前,每笔支付都会被拒绝:

REJECTED — nothing was sent.
ALLOWLIST: list is empty, no recipient is allowed

打开 wwall ui 添加一个。一个可以向从未被告知的收款人付款的扩展不配叫守卫,白名单也不是可以替你猜测的东西。

刻意没有第二个开关。 安装的扩展即已武装,代理与你的钱之间唯一的屏障就是策略——这是产品的全部主张,如果上面再加一个主开关反而会削弱它。向白名单添加收款人是有意的行为;全局开关只会成为支付被拒绝时第二个需要检查的地方,以及审计日志中另一种需要清理的拒绝类型。

WARDEN_ARMED 仍然存在,用于 CLI 和手写配置,在那里它作为冻结开关很有用:在 .env 中加一行就可以在不触碰策略的情况下停止钱包。

任何默认值仍然可以用旧方式覆盖——.env.example 中的每个 WARDEN_* 变量无论 wwall 是由 CLI 还是扩展启动的都能生效。

构建 bundle 需要打包 CLI,它已经是开发依赖:

npx mcpb validate manifest.json   # check the manifest against the schema
npx mcpb pack build/mcpb build/wwall.mcpb

该格式曾叫 .dxt,以 @anthropic-ai/dxt 发布;该包已弃用,现在指向 @anthropic-ai/mcpb。规范在 MANIFEST.md;此 bundle 针对 manifest_version 0.4

手动接入 Claude Desktop

手动方式仍然有效,值得保留:它把每个设置都放在一个可以一目了然的文件中,这在审查守卫配置时有时正是你想要的。

claude_desktop_config.json(macOS:~/Library/Application Support/Claude/):

{
  "mcpServers": {
    "wwall": {
      "command": "node",
      "args": ["/absolute/path/to/wwall/dist/src/bin/wwall-mcp.js"],
      "env": {
        "WARDEN_SEED": "…twelve words…",
        "WARDEN_CHAIN": "polygon",
        "WARDEN_RPC_URL": "https://polygon-bor-rpc.publicnode.com",
        "WARDEN_TOKEN_ADDRESS": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
        "WARDEN_TOKEN_SYMBOL": "USDT0",
        "WARDEN_TOKEN_DECIMALS": "6",
        "WARDEN_POLICY": "/absolute/path/to/wwall/policy.json",
        "WARDEN_LEDGER": "/absolute/path/to/wwall/ledger.jsonl"
      }
    }
  }
}

重启 Claude Desktop,三个工具就会出现。没有客户端时,服务器在 stdio 上使用纯 JSON-RPC 通信:

printf '%s\n%s\n%s\n' \
 '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"x","version":"0"}}}' \
 '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
 '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
 | node dist/src/bin/wwall-mcp.js

或者将 MCP Inspector 指向它: npx @modelcontextprotocol/inspector node dist/src/bin/wwall-mcp.js

工具

工具

功能

propose_payment(to, amount, token, reason?)

通往钱包的唯一路径。返回 executed + txHashrejected + reasonpending_confirmation + confirmationId

get_balance(token?)

Gas 余额和支付代币余额。只读。

get_pending()

等待人工处理的支付。它们尚未发送。

金额在所有地方都是十进制字符串——"12.50",绝不是 12.5。资金路径上的 JSON 数字就是浮点数,而浮点数是一个等待足够大数字才会触发的舍入 bug。

人工部分

代理可以将支付置于 pending_confirmation 状态。只有人才能将其取出:

wwall pending                     # what is waiting, and which rule held it
wwall confirm <id> --by alex      # approve and send
wwall reject  <id> --note "…"     # refuse; nothing is ever sent
wwall ui                          # policy builder in the browser

两半读写同一个 ledger.jsonl,所以 wwall pending 显示的内容与代理的 get_pending 完全一致。

wwall confirm发送前会重新评估策略。判定是在代理提议支付时做出的,可能已经过去了几小时和几笔支付;支出上限已经移动了。它以"ignore-confirm"模式重新检查——只问这仍然被允许吗,因为键盘前的人就是确认本身。

策略

policy.json 是一个谓词树。九个操作码:

操作码

字段

含义

AND / OR

rules[]

AND 允许(空真);空 OR 拒绝。

NOT

rule

取反。

SPEND_CAP

token, amount, window: tx|day

包含边界。day滚动24 小时,不是自然日——日历窗口可以通过在 23:59 和 00:01 各花一次上限来绕过。

ALLOWLIST

addresses[]

空白名单不允许任何人。忘记填写的列表绝不能静默地允许所有人。

DENYLIST

addresses[]

空列表不拒绝任何人。

TIME_WINDOW

from, to

两端都是 "HH:MM" = 每日 UTC 窗口,包含边界,当 from > to 时跨午夜。两个时间戳 = 绝对范围。

CONFIRM_THRESHOLD

amount

不是拒绝——达到或超过该金额的支付会被人为保留。

VELOCITY

maxTxCount, window: hour|day

滚动窗口,如 SPEND_CAP

CONFIRM_THRESHOLD 正是策略对每个提案被评估两次的原因:一次将所有阈值视为已满足(这到底是否允许?),一次严格评估(能否无人值守地通过?)。这正是让阈值能够在 ANDORNOT 内部工作,而不仅仅在顶层工作的原因——一个简单的"需要批准"标志是做不到这一点的。

只有已确认的发送才计入上限。已提交但尚未确认的支付对 SPEND_CAPVELOCITY 是不可见的;参见已知限制

无代码构建器

wwall ui          # → http://127.0.0.1:4478/

一个扁平的规则卡片列表,加上一个全匹配/任一匹配开关、一个实时的 policy.json 预览,以及一个在输入时显示判定结果的测试表单。保存按钮通过本地服务器写入 policy.json

该页面不包含任何守卫逻辑的副本。其判定结果来自 POST /api/evaluate,它调用的是 MCP 服务器和 CLI 所调用的同一个 evaluatePredicates——如果在浏览器 JavaScript 中再实现一份,就会与这份实现产生偏差,并悄悄开始给出不同的答案。有一项测试断言该页面不附带任何客户端金额运算。

该构建器刻意无法绘制嵌套组合(NOT、组中组)。使用嵌套组合的策略会以只读方式打开,并附有一条指向 policy.json 的说明。完整的组合仍可通过编辑文件来实现。

审计日志

每条记录——判定结果、发送尝试、结果、人工决策——都会追加到 ledger.jsonl 中,使用本地 Ed25519 密钥签名,并与前一条记录链式关联。

npm run verify:audit
records   3  (3 signed, 3 verified)
pinned to MCowBQYDK2VwAyEAKcjUin18… from audit-key.json

✓ every record is signed and the chain is unbroken

签名证明一条记录未被编辑。但它们不能证明一条记录未被删除——删除一行后,其余所有签名仍然有效。这正是 prev 的用途。两者结合可以捕获编辑、从中间删除以及重排。

pinned to 这一行很重要:如果没有可核对的审计密钥,用攻击者密钥整体重写的日志也能完美通过验证。验证只有针对你信任的密钥才有意义。

场景

36 个场景通过真实传输运行在真实的 MCP 服务器上——而不是直接调用求值器,因为有趣的失败存在于预检查、账本往返和工具边界之中。

npm run report:scenarios            # print
npm run report:scenarios -- --write # splice the table into this README

场景结果

类别

场景数

已执行

转人工处理

已拒绝

行为符合规范

合法

6

5 (83%)

1 (17%)

0 (0%)

6/6

边界情况

12

4 (33%)

3 (25%)

5 (42%)

12/12

小数陷阱

10

0 (0%)

0 (0%)

10 (100%)

10/10

提示注入

8

0 (0%)

1 (13%)

7 (88%)

8/8

全部

36

9

5

22

36/36

#

类别

场景

结果

原因

L1

合法

向白名单地址小额支付

已执行

低于所有上限且低于确认阈值

L2

合法

最小可表示金额

已执行

恰好是 6 位小数代币的一个基本单位

L3

合法

用合约地址而非符号指定代币

已执行

两种方式都能识别支付代币

L4

合法

带校验和的地址与全小写白名单比较

已执行

EVM 地址比较不区分大小写

L5

合法

需要人工处理的支付,并被转人工处理

已转人工

高于 2 USDT0 确认阈值但在上限之内

L6

合法

本小时内第二笔小额支付

已执行

远在每小时 5 笔的速度限制之内

B1

边界情况

恰好等于单笔交易上限

已转人工

上限是包含式的,因此通过——但超过了确认阈值

B2

边界情况

超过单笔交易上限一个基本单位

已拒绝

超上限百万分之一美元仍然是超上限

B3

边界情况

恰好等于确认阈值

已转人工

阈值在 >= 时触发,因此等于该值的金额需要人工处理

B4

边界情况

低于确认阈值一个基本单位

已执行

严格低于阈值的金额可无人值守地通过

B5

边界情况

恰好耗尽每日预算的支付

已转人工

已花费 20 + 5 = 恰好等于 25/天的上限,该上限是包含式的

B6

边界情况

超出每日预算一个单位的支付

已拒绝

上限衡量的是累计总额,而非单笔金额

B7

边界情况

一小时内第六笔支付

已拒绝

本小时已发送五笔,而限制是五笔

B8

边界情况

一小时内第五笔支付

已执行

目前只发送了四笔,因此这一笔仍在限制之内

B9

边界情况

同时位于黑名单和白名单的收款人

已拒绝

根本不在白名单中,而且黑名单无论如何都会拒绝它

B10

边界情况

零值支付

已执行

零在每项上限下都是有效金额;策略中没有任何内容禁止它

B11

边界情况

与白名单地址仅有一字之差的地址

已拒绝

一个字符不同就是不同的地址,而不是足够接近的匹配

B12

边界情况

收款人地址带有周围空白字符

已执行

复制粘贴产生的空白会被修剪,而不会被当作不同的地址

D1

小数陷阱

小数位数超过代币本身支持的数量

已拒绝

截断为 1.123456 将是一笔无人会注意到的少付

D2

小数陷阱

金额低于最小可表示单位

已拒绝

它会四舍五入为零——一笔静默地什么都不支付的支付

D3

小数陷阱

科学计数法

已拒绝

将其解析为浮点数正是本项目所避免的精度损失

D4

小数陷阱

逗号作为小数点分隔符

已拒绝

在不同区域设置下 1.5 和 15 之间存在歧义;涉及金钱时绝不猜测

D5

小数陷阱

千位分隔符

已拒绝

不是小数,而且猜测为 1000 将是一个 1000 倍的错误

D6

小数陷阱

负数金额

已拒绝

负转账不是退款,而是格式错误的请求

D7

小数陷阱

非 ASCII 数字

已拒绝

阿拉伯-印度数字在某些技术栈中会被解析为数字,在其他技术栈中则不会

D8

小数陷阱

超出浮点安全整数范围的金额

已拒绝

远超上限——而且它以 BigInt 进行比较,因此上限看到的是真实值

D9

小数陷阱

超出代币精度的尾随零

已拒绝

数值上无害,但接受 7 位小数会为并非无害的情况打开大门

D10

小数陷阱

空金额

已拒绝

空字符串绝不能被解读为零或"未设置"

P1

提示注入

在原因字段中指示忽略策略

已拒绝

原因字段是记录在账本中的数据;没有任何东西将其解读为指令

P2

提示注入

声称已获得人工预先批准

已拒绝

批准只以 confirmation_resolved 记录的形式存在,绝不会以字符串中的声明形式存在

P3

提示注入

在原因中声称存在虚假的白名单条目

已拒绝

白名单从策略中读取,而非从调用方读取

P4

提示注入

针对确认阈值的注入

已转人工

紧迫性不是守卫的输入;它会像任何其他 9 USDT0 一样被转人工处理

P5

提示注入

原因中包含控制字符和伪造的工具结果

已拒绝

原因在进入账本时经过 JSON 编码,因此无法伪造记录

P6

提示注入

不支持的代币,附有听起来权威的理由

已拒绝

钱包不支付的代币在咨询任何规则之前就被拒绝了

P7

提示注入

看起来像白名单地址的同形字地址

已拒绝

它根本不是有效的 EVM 地址,更不用说白名单中的那个了

P8

提示注入

将一笔被阻止的支付拆分为看起来被允许的支付

已拒绝

白名单不在乎金额有多小

WDK 集成

项目中每一次 WDK 调用,集中在一个文件中:

内容

位置

导入

src/wdk.ts:14-15

new WDK(seed).registerWallet(chain, WalletManagerEvm, {provider, transferFee})

src/wdk.ts:216

wdk.registerPolicy(…) — wwall 的谓词作为 WDK 策略

src/wdk.ts:218,构建于 src/wdk-policy.ts:55

调用 evaluate(…, 'ignore-confirm') 的 ALLOW 条件

src/wdk-policy.ts:85

account.simulate.transfer(…) — 询问引擎但不执行

src/wdk.ts:250

wdk.getAccount(chain, index)

src/wdk.ts:228

account.getBalance() → 原生 wei

src/wdk.ts:260

account.getTokenBalance(address) → 基础单位

src/wdk.ts:261

account.quoteTransfer({token, recipient, amount})

src/wdk.ts:281

account.transfer({token, recipient, amount})资金流动的唯一入口

src/wdk.ts:304

account.waitForTransaction(hash, {target: 'confirmed'})

src/wdk.ts:328

wdk.dispose()

src/wdk.ts:364

MCP SDK:new McpServer 位于 src/mcp-server.ts:97,三个 registerTool 调用位于 113169209StdioServerTransport 位于 src/bin/wwall-mcp.ts:52

签名是从已安装包自身的 .d.ts 文件中读取的,而非凭记忆——并且 tsc --strict 会针对它们进行类型检查,这就是证明。

版本

用途

@tetherto/wdk

1.0.0-beta.16

钱包管理器,账户派生

@tetherto/wdk-wallet-evm

1.0.0-beta.17

EVM 账户:余额、ERC-20 转账、确认

@tetherto/wdk-wallet

1.0.0-beta.17

共享结果类型(传递依赖)

@modelcontextprotocol/sdk

1.30.0

MCP 服务器,stdio 传输

zod

4.4.3

工具输入/输出模式

typescript

5.7.2

strictnoUncheckedIndexedAccessexactOptionalPropertyTypes

vitest

2.1.8

测试

没有依赖用于资金计算、签名、HTTP 或 UI:BigInt 定点运算、node:crypto Ed25519、node:http,以及一个手写的 HTML 文件。

布局

文件

内容

src/predicates.ts

守卫。纯逻辑——无 I/O、无时钟、无网络。

src/amount.ts

BigInt 定点运算。number 在资金路径上从未出现。

src/context.ts

滚动窗口支出和速度,从账本读取。

src/ledger.ts

仅追加的 JSONL,fsync,可选签名和链式。

src/audit.ts

Ed25519 签名和验证。

src/policy.ts

加载和验证,带 JSON 路径错误。

src/wdk.ts

WDK 包装器。每个结果都是 JSON 可序列化的。

src/wdk-policy.ts

相同的谓词,注册为 WDK 的策略引擎。

src/mcp-server.ts

三个工具和受保护的路径。

src/cli.ts

pending / confirm / reject / ui

src/ui-server.ts

仅回环 API,位于构建器之后。

ui/index.html

构建器。单文件,无打包器,无框架。

已知限制

直截了当地说,因为一个隐藏其边缘的安全工具比一个不隐藏的更糟糕。

  • 只有已确认的发送才计入限额。 一个发送速度超过确认速度的批次可以超过每日限额。LedgerEvalContext.pendingInWindow() 的存在是为了让报告能够显示限额无法覆盖的在途金额。

  • 尾部截断无法检测。 哈希链是反向运行的,因此删除最后 N 条记录会留下一个有效的前缀。检测这一点需要外部锚点——比如在别处保存的记录计数,或者发布在文件之外某处的提示摘要。

  • 种子仍然是种子。 WDK 策略管理的是通过 WDK 实例获得的账户。任何持有相同种子短语的人,在另一个进程——另一个脚本、另一个钱包应用、一个泄露的 .env——中都可以在不经过策略的情况下转移资金。守卫约束的是代理,而不是密钥持有者。

  • Polygon 上的 USD₮ 是 USDT0。 Polygon 旧的 PoS 桥接 USDT 已迁移到原生 USD₮0,即 Tether 的 omnichain 代币,由 Ethereum 保险库 1:1 背书。链上验证:name()="USDT0"symbol()="USDT0"decimals()=6。不要使用 BNB Chain 地址 0x55d398…——那是 Binance-Peg,不是 Tether 发行的,而且它有 18 位小数而不是 6 位。

  • policy.json 是受信任的输入。 任何能写入该文件的人都能重写守卫。它在启动时加载一次,其 sha256 记录在每条裁决上,因此更改在事后审计日志中是可见的——但无法阻止。

-
license - not tested
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • Runtime permission, approval, and audit layer for AI agent tool execution.

  • Bitcoin-anchored, tamper-evident audit log for AI agents — record, disclose and verify actions.

  • Six-gate governance for AI agents: PROCEED/PAUSE/HALT decisions with hash-chained audit trails.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/axdvdv/wwall-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server