walletcap 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- walletcap-0.2.3/.github/workflows/publish.yml +69 -0
- walletcap-0.2.3/CHANGELOG.md +58 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/PKG-INFO +27 -17
- {walletcap-0.2.2 → walletcap-0.2.3}/README.md +26 -16
- {walletcap-0.2.2 → walletcap-0.2.3}/js/agentic.mjs +89 -16
- {walletcap-0.2.2 → walletcap-0.2.3}/js/cli.mjs +2 -1
- {walletcap-0.2.2 → walletcap-0.2.3}/js/index.d.ts +6 -1
- {walletcap-0.2.2 → walletcap-0.2.3}/js/index.mjs +4 -1
- {walletcap-0.2.2 → walletcap-0.2.3}/js/package.json +2 -1
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/agentic.test.mjs +67 -3
- walletcap-0.2.3/js/tests/agentkit.test.mjs +80 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/fake-payments-mcp.mjs +4 -1
- walletcap-0.2.3/js/tests/types/agentkit.d.ts +10 -0
- walletcap-0.2.3/js/tests/types/recipe.ts +8 -0
- walletcap-0.2.3/js/tests/types/subclass.ts +13 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/x402.mjs +22 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/pyproject.toml +1 -1
- {walletcap-0.2.2 → walletcap-0.2.3}/solidity/README.md +15 -9
- {walletcap-0.2.2 → walletcap-0.2.3}/solidity/evm/check.mjs +35 -14
- {walletcap-0.2.2 → walletcap-0.2.3}/solidity/evm/package.json +1 -1
- {walletcap-0.2.2 → walletcap-0.2.3}/solidity/src/AccountCapSpender.sol +42 -37
- {walletcap-0.2.2 → walletcap-0.2.3}/solidity/test/AccountCapSpender.t.sol +45 -3
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/__init__.py +1 -1
- walletcap-0.2.2/.github/workflows/publish.yml +0 -35
- walletcap-0.2.2/CHANGELOG.md +0 -28
- {walletcap-0.2.2 → walletcap-0.2.3}/.circleci/config.yml +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/.github/workflows/ci.yml +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/.gitignore +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/LICENSE +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/NOTICE +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/SECURITY.md +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/docs/threat-model.md +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/docs/vendor-disclosure.md +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/examples/quickstart/README.md +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/examples/quickstart/quickstart.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/examples/quickstart/quickstart.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/LICENSE +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/NOTICE +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/README.md +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/_keccak.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/cdp.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/errors.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/ledger.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/money.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/budget.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/cdp-sdk.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/cdp.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/cli.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/keccak.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/money.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/processes.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/fake-awal/dist/index.js +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/fake-awal/dist/ipcClient.js +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/fake-awal/package.json +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/payer.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/race.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/x402-kit.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/support/x402-payer.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/x402-fetch.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/js/tests/x402.test.mjs +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/pytest.ini +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/scripts/ci.sh +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/solidity/foundry.toml +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/solidity/test/Mocks.sol +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/__main__.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/_keccak.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/budget.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/errors.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/ledger.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/money.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/src/walletcap/py.typed +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/tests/test_budget.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/tests/test_keccak.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/tests/test_ledger.py +0 -0
- {walletcap-0.2.2 → walletcap-0.2.3}/tests/test_money.py +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Publish walletcap to PyPI and npm when a GitHub Release is published, or by hand.
|
|
3
|
+
# Both use trusted publishing, so no token is stored. PyPI matches this repository,
|
|
4
|
+
# the workflow filename (publish.yml) and the "pypi" environment, and attaches PEP 740
|
|
5
|
+
# attestations. npm matches this repository, publish.yml and the "npm" environment,
|
|
6
|
+
# and attaches provenance.
|
|
7
|
+
name: Publish
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
pypi:
|
|
20
|
+
name: PyPI
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: pypi
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
25
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
- name: Check the release tag matches the package version
|
|
29
|
+
if: github.event_name == 'release'
|
|
30
|
+
env:
|
|
31
|
+
TAG: ${{ github.event.release.tag_name }}
|
|
32
|
+
run: test "$TAG" = "v$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")"
|
|
33
|
+
- name: Run the Python suite
|
|
34
|
+
run: |
|
|
35
|
+
python -m pip install --upgrade pytest
|
|
36
|
+
python -m pytest -q
|
|
37
|
+
- name: Build the distributions
|
|
38
|
+
run: |
|
|
39
|
+
python -m pip install --upgrade build
|
|
40
|
+
python -m build
|
|
41
|
+
- name: Publish to PyPI
|
|
42
|
+
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
43
|
+
|
|
44
|
+
npm:
|
|
45
|
+
name: npm
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
environment: npm
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
50
|
+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
51
|
+
with:
|
|
52
|
+
node-version: "22"
|
|
53
|
+
registry-url: "https://registry.npmjs.org"
|
|
54
|
+
package-manager-cache: false
|
|
55
|
+
- name: Use an npm that supports trusted publishing (11.5.1 or later)
|
|
56
|
+
run: npm install --global npm@11.21.1
|
|
57
|
+
- name: Check the release tag matches the package version
|
|
58
|
+
if: github.event_name == 'release'
|
|
59
|
+
env:
|
|
60
|
+
TAG: ${{ github.event.release.tag_name }}
|
|
61
|
+
run: test "$TAG" = "v$(node -p "require('./js/package.json').version")"
|
|
62
|
+
- name: Run the JavaScript suite
|
|
63
|
+
working-directory: js
|
|
64
|
+
run: |
|
|
65
|
+
npm install --no-audit --no-fund --no-package-lock
|
|
66
|
+
node --test
|
|
67
|
+
- name: Publish to npm with provenance
|
|
68
|
+
working-directory: js
|
|
69
|
+
run: npm publish --provenance
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
# Changelog
|
|
3
|
+
|
|
4
|
+
## 0.2.3 — 2026-11-06
|
|
5
|
+
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Releases are published from GitHub Actions with trusted publishing: npm with provenance,
|
|
9
|
+
PyPI with PEP 740 attestations. No token is stored. A GitHub Release whose tag doesn't
|
|
10
|
+
match the package version is refused.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `AccountCapSpender` no longer locks an account after a burst of payments. An unbounded log
|
|
15
|
+
made the first payment after 3,000 tiny ones cost about 17.7M gas, past the 2^24
|
|
16
|
+
per-transaction cap. The window is now counted in 24 buckets in a fixed ring of 25 slots, so
|
|
17
|
+
every `pay` costs the same gas. A payment counts for up to one bucket (window/24) past its
|
|
18
|
+
window, never less. The window must be a multiple of 24 seconds.
|
|
19
|
+
- `walletcap awal-mcp` ran `npx -y @coinbase/payments-mcp`, which is Coinbase's installer, not
|
|
20
|
+
the MCP server. It now runs the installed server, `~/.payments-mcp/bundle.js`, and says how to
|
|
21
|
+
install it when it is missing.
|
|
22
|
+
- `walletcap awal-mcp` forwarded every tool it didn't count. It now passes through only the
|
|
23
|
+
tools that move no money, and counts `send` and `make_http_request_with_x402`. It refuses
|
|
24
|
+
everything else, `trade` included, and leaves those tools out of `tools/list`, unless the
|
|
25
|
+
owner names one with `--allow-tool`. JSON-RPC batches and lines that aren't JSON are refused.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `budget.guardAgentKit(provider)` puts the budget on every x402 client an AgentKit
|
|
30
|
+
`X402ActionProvider` builds. The README's subclass recipe didn't compile in TypeScript,
|
|
31
|
+
where `createX402Client` is private. `tsc` now checks the recipe against AgentKit's declared
|
|
32
|
+
types in the test suite.
|
|
33
|
+
|
|
34
|
+
## 0.2.2 — 2026-11-05
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- A mixed-case EVM address is checked against EIP-55 before it is accepted. A bad
|
|
39
|
+
checksum is refused; all-lowercase and all-uppercase hex are still accepted.
|
|
40
|
+
- A Solana CAIP-10 address is reduced to its address, so it and the bare address
|
|
41
|
+
share one budget.
|
|
42
|
+
- Numeric `window` values are milliseconds in Python too, matching JavaScript.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- The rolling window cannot be shortened once the budget has spend. An edit can
|
|
47
|
+
change the cap and the per-payment limit, but it cannot reset the count by
|
|
48
|
+
shrinking the window.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- `SECURITY.md`, `docs/threat-model.md`, `docs/vendor-disclosure.md`, and the
|
|
53
|
+
Solidity audit status in `solidity/README.md`.
|
|
54
|
+
- `examples/quickstart/` in JavaScript and Python, run in CI. No wallet, chain,
|
|
55
|
+
keys or network.
|
|
56
|
+
- EIP-55, Solana CAIP-10, and window-freeze tests in both languages.
|
|
57
|
+
|
|
58
|
+
## 0.2.1 — 2026-11-05
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: walletcap
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: One spend budget per wallet for AI agents: counted across every process, replica, key, permission and payment rail.
|
|
5
5
|
Project-URL: Homepage, https://github.com/coriolislabs/WalletCap
|
|
6
6
|
Project-URL: Repository, https://github.com/coriolislabs/WalletCap
|
|
@@ -209,16 +209,23 @@ $ npx walletcap awal x402 pay https://api.example.com/data # instead of: npx a
|
|
|
209
209
|
$ npx walletcap awal send 0.05 0xRecipient --chain base # instead of: npx awal send …
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
-
For an MCP client such as Claude Desktop, start Coinbase's server behind walletcap.
|
|
213
|
-
`npx
|
|
212
|
+
For an MCP client such as Claude Desktop, start Coinbase's server behind walletcap. Install the
|
|
213
|
+
server once: `npx @coinbase/payments-mcp install --client other --no-auto-config` puts it in
|
|
214
|
+
`~/.payments-mcp`, and walletcap runs `~/.payments-mcp/bundle.js` from there. (`npx
|
|
215
|
+
@coinbase/payments-mcp` is the installer, not the server.) To run another server, put its
|
|
216
|
+
command after `--`.
|
|
214
217
|
|
|
215
218
|
```json
|
|
216
219
|
{ "mcpServers": { "payments": { "command": "npx", "args": ["walletcap", "awal-mcp", "--wallet", "0xYourAgentWallet"] } } }
|
|
217
220
|
```
|
|
218
221
|
|
|
219
222
|
- `send` and `x402 pay` (over MCP, `send` and `make_http_request_with_x402`) are checked against
|
|
220
|
-
the wallet's budget before the wallet signs anything, and counted. Every other command
|
|
221
|
-
|
|
223
|
+
the wallet's budget before the wallet signs anything, and counted. Every other CLI command is
|
|
224
|
+
the wallet's own.
|
|
225
|
+
- Over MCP, the tools that move no money (balance, address, sign-in, bazaar search, payment
|
|
226
|
+
discovery) pass through. Any other tool, `trade` or one the server adds later, is refused and
|
|
227
|
+
left out of the tool list: walletcap can't count it. `--allow-tool <name>` lets one through,
|
|
228
|
+
uncounted. JSON-RPC batches are refused.
|
|
222
229
|
- An x402 payment is priced from the endpoint's own 402 answer, and the wallet is capped at exactly
|
|
223
230
|
that amount, so it refuses to sign for more.
|
|
224
231
|
- A payment the payee confirms as settled counts for one window. One it doesn't confirm keeps
|
|
@@ -234,21 +241,20 @@ For an MCP client such as Claude Desktop, start Coinbase's server behind walletc
|
|
|
234
241
|
|
|
235
242
|
### Coinbase AgentKit
|
|
236
243
|
|
|
237
|
-
AgentKit's x402 actions build their own x402 client on every call.
|
|
238
|
-
|
|
239
|
-
is signed, whatever terms the merchant sends:
|
|
244
|
+
AgentKit's x402 actions build their own x402 client on every call. `budget.guardAgentKit()` puts
|
|
245
|
+
the budget on every client the provider builds, so each payment is checked against the wallet's
|
|
246
|
+
budget before it is signed, whatever terms the merchant sends:
|
|
240
247
|
|
|
241
248
|
```js
|
|
242
249
|
import { X402ActionProvider } from '@coinbase/agentkit';
|
|
243
250
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return budget.guard(await super.createX402Client(walletProvider));
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
// AgentKit.from({ walletProvider, actionProviders: [new BudgetedX402ActionProvider()] })
|
|
251
|
+
const x402 = budget.guardAgentKit(new X402ActionProvider());
|
|
252
|
+
// AgentKit.from({ walletProvider, actionProviders: [x402] })
|
|
250
253
|
```
|
|
251
254
|
|
|
255
|
+
It compiles in TypeScript, where a subclass can't override AgentKit's private
|
|
256
|
+
`createX402Client`. Tested against the shape of AgentKit 0.10.4.
|
|
257
|
+
|
|
252
258
|
### Coinbase Spend Permissions, on chain
|
|
253
259
|
|
|
254
260
|
For a limit the agent's own code can't go around, `solidity/src/AccountCapSpender.sol` is the
|
|
@@ -256,7 +262,9 @@ spender for Spend Permissions. Deploy it with the SpendPermissionManager, the ag
|
|
|
256
262
|
address, the cap, the window, the per-payment max and the payees allowed. Grant Spend
|
|
257
263
|
Permissions to the contract instead of the agent; the agent calls
|
|
258
264
|
`pay(permission, amount, payee)`. Every permission the account grants it then shares one
|
|
259
|
-
rolling cap.
|
|
265
|
+
rolling cap. The window is counted in 24 buckets, so every payment costs the same gas however
|
|
266
|
+
many came before it; a payment counts for up to one bucket (window/24) past its window, never
|
|
267
|
+
less.
|
|
260
268
|
|
|
261
269
|
## The owner's controls
|
|
262
270
|
|
|
@@ -316,7 +324,7 @@ only for a demo or a first-run setup.
|
|
|
316
324
|
| `tests/` | the Python suite, including the tests that drive both languages against one budget file |
|
|
317
325
|
|
|
318
326
|
```console
|
|
319
|
-
$ cd js && npm install && node --test #
|
|
327
|
+
$ cd js && npm install && node --test # 65 tests
|
|
320
328
|
$ uv run --with pytest pytest -q # 59 tests, from the repository root
|
|
321
329
|
$ cd solidity/evm && npm install && node check.mjs .. # the contract on an EVM, no Foundry needed
|
|
322
330
|
$ cd solidity && forge test # the contract's canonical tests, if you have Foundry
|
|
@@ -324,7 +332,9 @@ $ cd solidity && forge test # the contract's canonical test
|
|
|
324
332
|
|
|
325
333
|
The JavaScript suite runs the x402 guard against x402's own client and fetch wrapper, and runs
|
|
326
334
|
restarts and racing replicas as separate processes, and drives the Agentic Wallet wrapper and
|
|
327
|
-
MCP proxy against stand-ins for Coinbase's wallet and MCP server.
|
|
335
|
+
MCP proxy against stand-ins for Coinbase's wallet and MCP server. It builds AgentKit's x402
|
|
336
|
+
client the way AgentKit 0.10.4 does, and type-checks the AgentKit recipe with `tsc` against
|
|
337
|
+
AgentKit's declared types. The Python suite covers the ledger, the API,
|
|
328
338
|
the CLI and one budget shared between both languages. `solidity/evm/check.mjs` compiles the
|
|
329
339
|
contract with solc-js and drives it on an in-process EVM, covering what the Foundry tests cover.
|
|
330
340
|
|
|
@@ -186,16 +186,23 @@ $ npx walletcap awal x402 pay https://api.example.com/data # instead of: npx a
|
|
|
186
186
|
$ npx walletcap awal send 0.05 0xRecipient --chain base # instead of: npx awal send …
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
For an MCP client such as Claude Desktop, start Coinbase's server behind walletcap.
|
|
190
|
-
`npx
|
|
189
|
+
For an MCP client such as Claude Desktop, start Coinbase's server behind walletcap. Install the
|
|
190
|
+
server once: `npx @coinbase/payments-mcp install --client other --no-auto-config` puts it in
|
|
191
|
+
`~/.payments-mcp`, and walletcap runs `~/.payments-mcp/bundle.js` from there. (`npx
|
|
192
|
+
@coinbase/payments-mcp` is the installer, not the server.) To run another server, put its
|
|
193
|
+
command after `--`.
|
|
191
194
|
|
|
192
195
|
```json
|
|
193
196
|
{ "mcpServers": { "payments": { "command": "npx", "args": ["walletcap", "awal-mcp", "--wallet", "0xYourAgentWallet"] } } }
|
|
194
197
|
```
|
|
195
198
|
|
|
196
199
|
- `send` and `x402 pay` (over MCP, `send` and `make_http_request_with_x402`) are checked against
|
|
197
|
-
the wallet's budget before the wallet signs anything, and counted. Every other command
|
|
198
|
-
|
|
200
|
+
the wallet's budget before the wallet signs anything, and counted. Every other CLI command is
|
|
201
|
+
the wallet's own.
|
|
202
|
+
- Over MCP, the tools that move no money (balance, address, sign-in, bazaar search, payment
|
|
203
|
+
discovery) pass through. Any other tool, `trade` or one the server adds later, is refused and
|
|
204
|
+
left out of the tool list: walletcap can't count it. `--allow-tool <name>` lets one through,
|
|
205
|
+
uncounted. JSON-RPC batches are refused.
|
|
199
206
|
- An x402 payment is priced from the endpoint's own 402 answer, and the wallet is capped at exactly
|
|
200
207
|
that amount, so it refuses to sign for more.
|
|
201
208
|
- A payment the payee confirms as settled counts for one window. One it doesn't confirm keeps
|
|
@@ -211,21 +218,20 @@ For an MCP client such as Claude Desktop, start Coinbase's server behind walletc
|
|
|
211
218
|
|
|
212
219
|
### Coinbase AgentKit
|
|
213
220
|
|
|
214
|
-
AgentKit's x402 actions build their own x402 client on every call.
|
|
215
|
-
|
|
216
|
-
is signed, whatever terms the merchant sends:
|
|
221
|
+
AgentKit's x402 actions build their own x402 client on every call. `budget.guardAgentKit()` puts
|
|
222
|
+
the budget on every client the provider builds, so each payment is checked against the wallet's
|
|
223
|
+
budget before it is signed, whatever terms the merchant sends:
|
|
217
224
|
|
|
218
225
|
```js
|
|
219
226
|
import { X402ActionProvider } from '@coinbase/agentkit';
|
|
220
227
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return budget.guard(await super.createX402Client(walletProvider));
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
// AgentKit.from({ walletProvider, actionProviders: [new BudgetedX402ActionProvider()] })
|
|
228
|
+
const x402 = budget.guardAgentKit(new X402ActionProvider());
|
|
229
|
+
// AgentKit.from({ walletProvider, actionProviders: [x402] })
|
|
227
230
|
```
|
|
228
231
|
|
|
232
|
+
It compiles in TypeScript, where a subclass can't override AgentKit's private
|
|
233
|
+
`createX402Client`. Tested against the shape of AgentKit 0.10.4.
|
|
234
|
+
|
|
229
235
|
### Coinbase Spend Permissions, on chain
|
|
230
236
|
|
|
231
237
|
For a limit the agent's own code can't go around, `solidity/src/AccountCapSpender.sol` is the
|
|
@@ -233,7 +239,9 @@ spender for Spend Permissions. Deploy it with the SpendPermissionManager, the ag
|
|
|
233
239
|
address, the cap, the window, the per-payment max and the payees allowed. Grant Spend
|
|
234
240
|
Permissions to the contract instead of the agent; the agent calls
|
|
235
241
|
`pay(permission, amount, payee)`. Every permission the account grants it then shares one
|
|
236
|
-
rolling cap.
|
|
242
|
+
rolling cap. The window is counted in 24 buckets, so every payment costs the same gas however
|
|
243
|
+
many came before it; a payment counts for up to one bucket (window/24) past its window, never
|
|
244
|
+
less.
|
|
237
245
|
|
|
238
246
|
## The owner's controls
|
|
239
247
|
|
|
@@ -293,7 +301,7 @@ only for a demo or a first-run setup.
|
|
|
293
301
|
| `tests/` | the Python suite, including the tests that drive both languages against one budget file |
|
|
294
302
|
|
|
295
303
|
```console
|
|
296
|
-
$ cd js && npm install && node --test #
|
|
304
|
+
$ cd js && npm install && node --test # 65 tests
|
|
297
305
|
$ uv run --with pytest pytest -q # 59 tests, from the repository root
|
|
298
306
|
$ cd solidity/evm && npm install && node check.mjs .. # the contract on an EVM, no Foundry needed
|
|
299
307
|
$ cd solidity && forge test # the contract's canonical tests, if you have Foundry
|
|
@@ -301,7 +309,9 @@ $ cd solidity && forge test # the contract's canonical test
|
|
|
301
309
|
|
|
302
310
|
The JavaScript suite runs the x402 guard against x402's own client and fetch wrapper, and runs
|
|
303
311
|
restarts and racing replicas as separate processes, and drives the Agentic Wallet wrapper and
|
|
304
|
-
MCP proxy against stand-ins for Coinbase's wallet and MCP server.
|
|
312
|
+
MCP proxy against stand-ins for Coinbase's wallet and MCP server. It builds AgentKit's x402
|
|
313
|
+
client the way AgentKit 0.10.4 does, and type-checks the AgentKit recipe with `tsc` against
|
|
314
|
+
AgentKit's declared types. The Python suite covers the ledger, the API,
|
|
305
315
|
the CLI and one budget shared between both languages. `solidity/evm/check.mjs` compiles the
|
|
306
316
|
contract with solc-js and drives it on an in-process EVM, covering what the Foundry tests cover.
|
|
307
317
|
|
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// signs it, and the wallet is capped at that amount. A confirmed settlement counts for one
|
|
7
7
|
// window; a payment the payee never confirms keeps counting until its owner releases it.
|
|
8
8
|
import { spawn, spawnSync } from 'node:child_process';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
9
10
|
import { existsSync } from 'node:fs';
|
|
10
11
|
import { createRequire } from 'node:module';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
11
13
|
import { dirname, join, resolve } from 'node:path';
|
|
12
14
|
import { createInterface } from 'node:readline';
|
|
13
15
|
import { pathToFileURL } from 'node:url';
|
|
@@ -338,11 +340,28 @@ export async function awalMain(argv, { env = process.env, stdout = process.stdou
|
|
|
338
340
|
}
|
|
339
341
|
|
|
340
342
|
// ---------------------------------------------------------------------------------------------
|
|
341
|
-
// npx walletcap awal-mcp [--wallet <address>] [--path <file>] [-- <server command>]:
|
|
342
|
-
//
|
|
343
|
+
// npx walletcap awal-mcp [--wallet <address>] [--path <file>] [--allow-tool <name>]... [-- <server command>]:
|
|
344
|
+
// Coinbase's Agentic Wallet MCP server behind a proxy that counts its send and
|
|
345
|
+
// make_http_request_with_x402 calls, and refuses every tool it does not know.
|
|
343
346
|
|
|
344
347
|
const MCP_PAYMENT_TOOLS = { make_http_request_with_x402: 'make-x402-request', send: 'send-transaction' };
|
|
345
|
-
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The server's tools that move no money (payments-mcp 2.12.2), passed through as they are. Any
|
|
351
|
+
* other tool, `trade` included, is refused unless the wallet's owner names it with --allow-tool:
|
|
352
|
+
* a tool walletcap does not know may move money it cannot count.
|
|
353
|
+
*/
|
|
354
|
+
export const MCP_PASS_THROUGH_TOOLS = Object.freeze([
|
|
355
|
+
'bazaar_get_resource_details', 'bazaar_list', 'bazaar_search', 'check_session_status',
|
|
356
|
+
'get_wallet_address', 'get_wallet_balance', 'show_wallet_app', 'sign_in_with_email',
|
|
357
|
+
'verify_email_otp', 'x402_discover_payment_requirements',
|
|
358
|
+
]);
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Where Coinbase's server runs from. `npx @coinbase/payments-mcp` is its installer, not the
|
|
362
|
+
* server: it downloads the server to ~/.payments-mcp, and MCP clients run bundle.js with node.
|
|
363
|
+
*/
|
|
364
|
+
export const paymentsMcpServer = (home = homedir()) => [process.execPath, join(home, '.payments-mcp', 'bundle.js')];
|
|
346
365
|
|
|
347
366
|
const toolResult = raw => {
|
|
348
367
|
const text = raw?.content?.find(item => item?.type === 'text')?.text;
|
|
@@ -356,18 +375,38 @@ const toolResult = raw => {
|
|
|
356
375
|
}
|
|
357
376
|
};
|
|
358
377
|
|
|
359
|
-
/**
|
|
360
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Proxy an MCP client (stdin/stdout) to Coinbase's Agentic Wallet MCP server. Payments are
|
|
380
|
+
* counted, the tools in MCP_PASS_THROUGH_TOOLS and --allow-tool pass through, and every other
|
|
381
|
+
* tool is refused and left out of tools/list. Resolves with the server's exit code.
|
|
382
|
+
*/
|
|
383
|
+
export async function awalMcpMain(argv, { env = process.env, input = process.stdin, output = process.stdout, stderr = process.stderr, home } = {}) {
|
|
361
384
|
const split = argv.indexOf('--');
|
|
362
385
|
const own = split < 0 ? argv : argv.slice(0, split);
|
|
363
|
-
const
|
|
364
|
-
|
|
386
|
+
const { values } = parseArgs({
|
|
387
|
+
args: own,
|
|
388
|
+
options: { wallet: { type: 'string' }, path: { type: 'string' }, 'allow-tool': { type: 'string', multiple: true } },
|
|
389
|
+
strict: true,
|
|
390
|
+
});
|
|
391
|
+
const server = split < 0 ? paymentsMcpServer(home) : argv.slice(split + 1);
|
|
392
|
+
if (server.length === 0) {
|
|
393
|
+
stderr.write('walletcap: give the MCP server command after --\n');
|
|
394
|
+
return 2;
|
|
395
|
+
}
|
|
396
|
+
if (split < 0 && !existsSync(server[1])) {
|
|
397
|
+
stderr.write(`walletcap: Coinbase's Agentic Wallet MCP server is not installed at ${server[1]}. `
|
|
398
|
+
+ 'Install it once with: npx @coinbase/payments-mcp install --client other --no-auto-config '
|
|
399
|
+
+ '(or give walletcap the server command after --)\n');
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
const passed = new Set([...MCP_PASS_THROUGH_TOOLS, ...(values['allow-tool'] ?? [])]);
|
|
403
|
+
const offered = name => Object.hasOwn(MCP_PAYMENT_TOOLS, name) || passed.has(name);
|
|
365
404
|
const child = spawn(server[0], server.slice(1), { stdio: ['pipe', 'pipe', 'inherit'], env });
|
|
366
405
|
const write = (stream, message) => stream.write(`${JSON.stringify(message)}\n`);
|
|
367
406
|
const pending = new Map();
|
|
368
|
-
|
|
407
|
+
const listing = new Set(); // the client's tools/list requests, whose answers are filtered
|
|
369
408
|
const callTool = (name, args) => new Promise((resolveCall, rejectCall) => {
|
|
370
|
-
const id = `walletcap-${
|
|
409
|
+
const id = `walletcap-${randomUUID()}`;
|
|
371
410
|
pending.set(id, { resolve: resolveCall, reject: rejectCall });
|
|
372
411
|
write(child.stdin, { jsonrpc: '2.0', id, method: 'tools/call', params: { name, arguments: args } });
|
|
373
412
|
});
|
|
@@ -376,10 +415,18 @@ export async function awalMcpMain(argv, { env = process.env, input = process.std
|
|
|
376
415
|
let message;
|
|
377
416
|
try { message = JSON.parse(line); } catch { output.write(`${line}\n`); return; }
|
|
378
417
|
const waiting = pending.get(message?.id);
|
|
379
|
-
if (waiting
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
418
|
+
if (waiting !== undefined) {
|
|
419
|
+
pending.delete(message.id);
|
|
420
|
+
if (message.error) waiting.reject(new Error(message.error.message ?? JSON.stringify(message.error)));
|
|
421
|
+
else waiting.resolve(message.result);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
const key = message?.id === undefined ? undefined : JSON.stringify(message.id);
|
|
425
|
+
if (key !== undefined && listing.delete(key) && Array.isArray(message.result?.tools)) {
|
|
426
|
+
write(output, { ...message, result: { ...message.result, tools: message.result.tools.filter(tool => offered(tool?.name)) } });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
output.write(`${line}\n`);
|
|
383
430
|
});
|
|
384
431
|
|
|
385
432
|
// The guard talks to the wallet through the server's own tools, one guard per call so concurrent
|
|
@@ -419,11 +466,37 @@ export async function awalMcpMain(argv, { env = process.env, input = process.std
|
|
|
419
466
|
|
|
420
467
|
createInterface({ input }).on('line', line => {
|
|
421
468
|
let message;
|
|
422
|
-
try {
|
|
423
|
-
|
|
424
|
-
|
|
469
|
+
try {
|
|
470
|
+
message = JSON.parse(line);
|
|
471
|
+
} catch {
|
|
472
|
+
stderr.write('walletcap: dropped a line from the MCP client that is not JSON\n');
|
|
425
473
|
return;
|
|
426
474
|
}
|
|
475
|
+
if (Array.isArray(message)) {
|
|
476
|
+
// A batch would carry calls past the checks below; MCP dropped batches in 2025-06-18.
|
|
477
|
+
write(output, { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'walletcap does not forward JSON-RPC batches' } });
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (message?.method === 'tools/call') {
|
|
481
|
+
const name = String(message.params?.name);
|
|
482
|
+
if (Object.hasOwn(MCP_PAYMENT_TOOLS, name)) {
|
|
483
|
+
handle(message).catch(error => stderr.write(`walletcap: ${error.message}\n`));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (!passed.has(name)) {
|
|
487
|
+
write(output, {
|
|
488
|
+
jsonrpc: '2.0',
|
|
489
|
+
id: message.id,
|
|
490
|
+
result: {
|
|
491
|
+
content: [{ type: 'text', text: `walletcap refused ${name}: it can't tell whether this tool moves money, so it can't count it. `
|
|
492
|
+
+ `The wallet's owner can let it through, uncounted, with --allow-tool ${name}` }],
|
|
493
|
+
isError: true,
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (message?.method === 'tools/list' && message.id !== undefined && message.id !== null) listing.add(JSON.stringify(message.id));
|
|
427
500
|
child.stdin.write(`${line}\n`);
|
|
428
501
|
}).on('close', () => child.stdin.end());
|
|
429
502
|
|
|
@@ -24,7 +24,8 @@ Commands
|
|
|
24
24
|
history The newest payments first
|
|
25
25
|
release <id> Give back an in-flight payment you know never left the wallet
|
|
26
26
|
awal <command> Coinbase's Agentic Wallet CLI, with send and x402 pay counted in the budget
|
|
27
|
-
awal-mcp [-- <cmd>] Coinbase's payments-mcp
|
|
27
|
+
awal-mcp [-- <cmd>] Coinbase's Agentic Wallet MCP server (~/.payments-mcp) behind a proxy that
|
|
28
|
+
counts its payments and refuses tools it can't count (--allow-tool <name>)
|
|
28
29
|
|
|
29
30
|
Options
|
|
30
31
|
--wallet <address> The wallet the money leaves from (optional when the file holds one budget)
|
|
@@ -162,12 +162,17 @@ export declare class Budget {
|
|
|
162
162
|
history(options?: { limit?: number }): Payment[];
|
|
163
163
|
/** Count every payment an x402 client (@x402/core 2.x) makes in this budget. Returns the client. */
|
|
164
164
|
guard<C extends X402HookedClient>(client: C, options?: { assets?: AssetEntry[] }): C;
|
|
165
|
+
/**
|
|
166
|
+
* Count every payment a Coinbase AgentKit X402ActionProvider makes in this budget: each x402
|
|
167
|
+
* client the provider builds is guarded before it can sign. Returns the provider.
|
|
168
|
+
*/
|
|
169
|
+
guardAgentKit<P extends object>(provider: P, options?: { assets?: AssetEntry[] }): P;
|
|
165
170
|
/** A SpendStore for Coinbase's CDP applySpendControls that counts in this budget. */
|
|
166
171
|
cdpStore(options: { asset: string; scale?: bigint | number; clock?: () => number }): SharedBudgetStore;
|
|
167
172
|
close(): void;
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
/** Open the wallet's budget
|
|
175
|
+
/** Open the wallet's budget. Its owner creates it with `walletcap set`; pass create: true only for a demo. */
|
|
171
176
|
export declare function openBudget(options: BudgetOptions): Budget;
|
|
172
177
|
|
|
173
178
|
/** Dollars as micro-dollars. */
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
defaultBudgetPath, describeLimits, expandHome, formatUsd, formatWindow, limitFlags, MAX_AMOUNT,
|
|
18
18
|
normalizeWallet, parseLimits, shortWallet, toMicros,
|
|
19
19
|
} from './money.mjs';
|
|
20
|
-
import { guardX402Client, USD_ASSETS } from './x402.mjs';
|
|
20
|
+
import { guardX402ActionProvider, guardX402Client, USD_ASSETS } from './x402.mjs';
|
|
21
21
|
|
|
22
22
|
export {
|
|
23
23
|
BudgetError, BudgetRefused, defaultBudgetPath, formatUsd, MAX_AMOUNT, notSent, SCHEMA_VERSION,
|
|
@@ -304,6 +304,9 @@ export class Budget {
|
|
|
304
304
|
/** Count every payment an x402 client (@x402/core 2.x) makes in this budget. Returns the client. */
|
|
305
305
|
guard(client, options) { return guardX402Client(client, this, options); }
|
|
306
306
|
|
|
307
|
+
/** Count every payment a Coinbase AgentKit X402ActionProvider makes in this budget. Returns the provider. */
|
|
308
|
+
guardAgentKit(provider, options) { return guardX402ActionProvider(provider, this, options); }
|
|
309
|
+
|
|
307
310
|
/** A SpendStore for Coinbase's CDP applySpendControls, counting in this budget. For a 6-decimal USD stablecoin. */
|
|
308
311
|
cdpStore({ asset, scale = 1n, clock = Date.now } = {}) {
|
|
309
312
|
return new SharedBudgetStore({ path: this.#path, instrument: this.#wallet, asset, scale, clock });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "walletcap",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "One spend budget per wallet for AI agents: counted across every process, replica, key, permission and payment rail. Restarts don't reset it, limit changes don't zero it, and if it can't be read nothing is paid.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Coriolis Labs Inc.",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@x402/extensions": "2.28.0",
|
|
56
56
|
"@x402/fetch": "2.28.0",
|
|
57
57
|
"@x402/svm": "2.28.0",
|
|
58
|
+
"typescript": "5.6.3",
|
|
58
59
|
"viem": "2.37.13"
|
|
59
60
|
},
|
|
60
61
|
"repository": {
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// server are stand-ins here; the payee is a local HTTP server that answers 402 like an x402 one.
|
|
4
4
|
import assert from 'node:assert/strict';
|
|
5
5
|
import { execFile, spawn, spawnSync } from 'node:child_process';
|
|
6
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { createServer } from 'node:http';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { createInterface } from 'node:readline';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
12
|
import { after, before, test } from 'node:test';
|
|
13
13
|
import { BudgetRefused, openBudget } from '../index.mjs';
|
|
14
14
|
import { guardAgenticWallet, usdcAtomic } from '../agentic.mjs';
|
|
@@ -202,7 +202,8 @@ test('npx walletcap awal-mcp: the MCP server answers, and payments past the cap
|
|
|
202
202
|
};
|
|
203
203
|
try {
|
|
204
204
|
assert.equal((await ask(1, 'initialize', {})).serverInfo.name, 'payments-mcp');
|
|
205
|
-
|
|
205
|
+
// The server also offers trade and a tool walletcap has never seen: neither is listed.
|
|
206
|
+
assert.deepEqual((await ask(2, 'tools/list', {})).tools.map(t => t.name), ['make_http_request_with_x402', 'send', 'get_wallet_balance']);
|
|
206
207
|
const pay = { baseURL: base, path: '/pay?ttl=300', method: 'GET' };
|
|
207
208
|
const results = [];
|
|
208
209
|
for (let id = 3; id < 7; id += 1) results.push(await ask(id, 'tools/call', { name: 'make_http_request_with_x402', arguments: pay }));
|
|
@@ -211,6 +212,12 @@ test('npx walletcap awal-mcp: the MCP server answers, and payments past the cap
|
|
|
211
212
|
const sent = await ask(7, 'tools/call', { name: 'send', arguments: { recipient: PAY_TO, amountAtomic: '50000', chain: 'base-sepolia' } });
|
|
212
213
|
assert.equal(sent.isError, true);
|
|
213
214
|
assert.equal((await ask(8, 'tools/call', { name: 'get_wallet_balance', arguments: {} })).content.length, 1);
|
|
215
|
+
// A tool walletcap can't count is refused before it reaches the wallet.
|
|
216
|
+
for (const [id, name] of [[9, 'trade'], [10, 'drain_wallet']]) {
|
|
217
|
+
const refused = await ask(id, 'tools/call', { name, arguments: { amount: '1000' } });
|
|
218
|
+
assert.equal(refused.isError, true);
|
|
219
|
+
assert.match(refused.content[0].text, new RegExp(`walletcap refused ${name}.*--allow-tool ${name}`));
|
|
220
|
+
}
|
|
214
221
|
} finally {
|
|
215
222
|
proxy.stdin.end();
|
|
216
223
|
await new Promise(done => proxy.on('exit', done));
|
|
@@ -222,3 +229,60 @@ test('npx walletcap awal-mcp: the MCP server answers, and payments past the cap
|
|
|
222
229
|
assert.equal(budget.status().spent, 150_000n);
|
|
223
230
|
budget.close();
|
|
224
231
|
});
|
|
232
|
+
|
|
233
|
+
// Drive `npx walletcap awal-mcp` over stdio: send lines, collect answers by id.
|
|
234
|
+
const mcpProxy = (args, env) => {
|
|
235
|
+
const proxy = spawn(process.execPath, [CLI, 'awal-mcp', ...args], { env: { ...process.env, ...env }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
236
|
+
const replies = new Map();
|
|
237
|
+
let stderr = '';
|
|
238
|
+
proxy.stderr.on('data', chunk => { stderr += chunk; });
|
|
239
|
+
createInterface({ input: proxy.stdout }).on('line', line => { const m = JSON.parse(line); replies.set(m.id, m); });
|
|
240
|
+
const send = line => proxy.stdin.write(`${line}\n`);
|
|
241
|
+
const reply = async id => {
|
|
242
|
+
for (let i = 0; i < 200 && !replies.has(id); i += 1) await new Promise(done => setTimeout(done, 25));
|
|
243
|
+
assert.ok(replies.has(id), `no reply to ${id}; stderr: ${stderr}`);
|
|
244
|
+
return replies.get(id);
|
|
245
|
+
};
|
|
246
|
+
const ask = async (id, method, params) => {
|
|
247
|
+
send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
|
|
248
|
+
return (await reply(id)).result;
|
|
249
|
+
};
|
|
250
|
+
const close = async () => {
|
|
251
|
+
proxy.stdin.end();
|
|
252
|
+
return new Promise(done => proxy.on('exit', code => done({ code, stderr })));
|
|
253
|
+
};
|
|
254
|
+
return { ask, send, reply, close };
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
test('npx walletcap awal-mcp runs the server Coinbase installed, not its installer', async () => {
|
|
258
|
+
const home = mkdtempSync(join(DIR, 'home-'));
|
|
259
|
+
const path = join(DIR, 'mcp-default.sqlite');
|
|
260
|
+
const log = join(DIR, 'mcp-default.log');
|
|
261
|
+
writeFileSync(log, '');
|
|
262
|
+
openBudget({ wallet: WALLET, path, perPayment: '$0.05', perDay: '$0.15', create: true }).close();
|
|
263
|
+
const env = { HOME: home, USERPROFILE: home, FAKE_MCP_LOG: log };
|
|
264
|
+
|
|
265
|
+
// Before Coinbase's installer has run, there is no server to start: say so, start nothing.
|
|
266
|
+
const missing = await mcpProxy(['--wallet', WALLET, '--path', path], env).close();
|
|
267
|
+
assert.equal(missing.code, 1);
|
|
268
|
+
assert.match(missing.stderr, /npx @coinbase\/payments-mcp install/);
|
|
269
|
+
|
|
270
|
+
// The installer leaves ~/.payments-mcp/bundle.js, an ES module MCP clients run with node.
|
|
271
|
+
mkdirSync(join(home, '.payments-mcp'));
|
|
272
|
+
writeFileSync(join(home, '.payments-mcp', 'package.json'), '{"type":"module"}');
|
|
273
|
+
writeFileSync(join(home, '.payments-mcp', 'bundle.js'), `import ${JSON.stringify(pathToFileURL(FAKE_MCP).href)};\n`);
|
|
274
|
+
const mcp = mcpProxy(['--wallet', WALLET, '--path', path, '--allow-tool', 'trade'], env);
|
|
275
|
+
try {
|
|
276
|
+
assert.equal((await mcp.ask(1, 'initialize', {})).serverInfo.name, 'payments-mcp');
|
|
277
|
+
// --allow-tool lets trade through, uncounted; a tool nobody named stays out.
|
|
278
|
+
assert.deepEqual((await mcp.ask(2, 'tools/list', {})).tools.map(t => t.name), ['make_http_request_with_x402', 'send', 'get_wallet_balance', 'trade']);
|
|
279
|
+
assert.notEqual((await mcp.ask(3, 'tools/call', { name: 'trade', arguments: {} })).isError, true);
|
|
280
|
+
assert.equal((await mcp.ask(4, 'tools/call', { name: 'drain_wallet', arguments: {} })).isError, true);
|
|
281
|
+
// A JSON-RPC batch could carry a call past those checks, so it is refused whole.
|
|
282
|
+
mcp.send(JSON.stringify([{ jsonrpc: '2.0', id: 5, method: 'tools/call', params: { name: 'drain_wallet', arguments: {} } }]));
|
|
283
|
+
assert.equal((await mcp.reply(null)).error.code, -32600);
|
|
284
|
+
} finally {
|
|
285
|
+
await mcp.close();
|
|
286
|
+
}
|
|
287
|
+
assert.deepEqual(readFileSync(log, 'utf8').trim().split('\n').map(line => JSON.parse(line).name), ['trade']);
|
|
288
|
+
});
|