fix: record auto-approved spend#3
Conversation
|
Thanks, this is a real bug and the direction is right. Auto-approved payments still need to debit daily budget even when there is no wallet session. Before we merge, can you tighten the regression coverage? |
2676440 to
5b02a73
Compare
|
Updated, thanks. I tightened the regression coverage around the accounting invariant:
I also rebased onto current
|
|
Thanks, this is the right direction and it addresses the accounting bug we were worried about: auto-approved spend now gets recorded even when there is no user-approved wallet session. The tests for both sign_x402 and sign, plus repeated under-threshold spend hitting BUDGET_EXCEEDED_DAILY, are exactly the invariants we need. One important blocker before merge: the budget ledger must stay internal only. Right now the fallback creates a normal active agent session with granted_scopes = [internal.budget.ledger], and /agents returns all active sessions. That can leak this synthetic ledger into Desktop/Mobile as if it were a real connected agent, which is bad UX and could confuse users/security review. Please update this so internal budget accounting does not appear as a user-facing agent/session. A clean fix would be to filter sessions whose scopes are only internal.* out of /agents, while keeping recordSpend/checkBudget behavior unchanged. Also please add a regression assertion that after an auto-approved spend, /agents does not expose the internal.budget.ledger session to clients. Security invariant remains: internal.budget.ledger must only be usable for budget accounting. It must not become user-grantable, must not satisfy wallet/sign scopes, and must not show up as a connected agent in UI-facing APIs. |
|
Pushed the follow-up in Changes:
Verification:
|
055d963 to
6d0eb7e
Compare
|
Updated the branch for the internal-session blocker and rebased onto current main. Changes in the latest push:
Local validation under Node 22.22.3:
|
Summary
Validation
RUN v3.2.4 /Users/tatelyman/Documents/New project 7/dcp/packages/dcp-vault
✓ tests/policy-engine.test.ts (11 tests) 5ms
✓ tests/agent-registry.test.ts (18 tests) 5ms
✓ tests/nonce-store.test.ts (11 tests) 8ms
✓ tests/permission-store.test.ts (26 tests) 7ms
✓ tests/validation.test.ts (25 tests) 11ms
[19:52:13] INFO: incoming request
reqId: "req-1"
req: {
"method": "GET",
"url": "/health",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-1"
res: {
"statusCode": 200
}
responseTime: 1.4485830068588257
[19:52:13] INFO: incoming request
reqId: "req-2"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-2"
res: {
"statusCode": 200
}
responseTime: 115.25916695594788
[19:52:13] INFO: incoming request
reqId: "req-3"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-3"
res: {
"statusCode": 200
}
responseTime: 113.05279099941254
[19:52:13] INFO: incoming request
reqId: "req-4"
req: {
"method": "POST",
"url": "/v1/vault/sign_x402",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-4"
res: {
"statusCode": 200
}
responseTime: 17.73574995994568
[19:52:13] INFO: incoming request
reqId: "req-5"
req: {
"method": "POST",
"url": "/v1/vault/sign_x402",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-5"
res: {
"statusCode": 400
}
responseTime: 0.6247080564498901
[19:52:13] INFO: incoming request
reqId: "req-6"
req: {
"method": "POST",
"url": "/v1/vault/sign_x402",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-6"
res: {
"statusCode": 200
}
responseTime: 2.816249966621399
[19:52:13] INFO: incoming request
reqId: "req-7"
req: {
"method": "GET",
"url": "/budget/check?amount=0¤cy=USDC&chain=solana",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-7"
res: {
"statusCode": 200
}
responseTime: 0.18445801734924316
[19:52:13] INFO: incoming request
reqId: "req-8"
req: {
"method": "POST",
"url": "/v1/vault/unlock-mcp",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-8"
res: {
"statusCode": 200
}
responseTime: 0.4420830011367798
[19:52:13] INFO: incoming request
reqId: "req-9"
req: {
"method": "POST",
"url": "/v1/vault/lock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-9"
res: {
"statusCode": 200
}
responseTime: 0.4309999942779541
[19:52:13] INFO: incoming request
reqId: "req-a"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-a"
res: {
"statusCode": 400
}
responseTime: 111.64408302307129
[19:52:13] INFO: incoming request
reqId: "req-b"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-b"
res: {
"statusCode": 400
}
responseTime: 112.92166602611542
[19:52:13] INFO: incoming request
reqId: "req-c"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-c"
res: {
"statusCode": 400
}
responseTime: 110.95754098892212
[19:52:13] INFO: incoming request
reqId: "req-d"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-d"
res: {
"statusCode": 400
}
responseTime: 110.21525001525879
[19:52:13] INFO: incoming request
reqId: "req-e"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-e"
res: {
"statusCode": 429
}
responseTime: 109.11120796203613
[19:52:13] INFO: incoming request
reqId: "req-f"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-f"
res: {
"statusCode": 429
}
responseTime: 0.278999924659729
[19:52:13] INFO: incoming request
reqId: "req-g"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-g"
res: {
"statusCode": 429
}
responseTime: 0.1553339958190918
[19:52:13] INFO: incoming request
reqId: "req-h"
req: {
"method": "POST",
"url": "/v1/vault/unlock",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-h"
res: {
"statusCode": 429
}
responseTime: 0.14808297157287598
[19:52:13] INFO: incoming request
reqId: "req-i"
req: {
"method": "GET",
"url": "/scopes",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-i"
res: {
"statusCode": 200
}
responseTime: 0.15012502670288086
[19:52:13] INFO: incoming request
reqId: "req-j"
req: {
"method": "GET",
"url": "/agents",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-j"
res: {
"statusCode": 200
}
responseTime: 0.1253749132156372
[19:52:13] INFO: incoming request
reqId: "req-k"
req: {
"method": "GET",
"url": "/consent",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-k"
res: {
"statusCode": 200
}
responseTime: 0.08424997329711914
[19:52:13] INFO: incoming request
reqId: "req-l"
req: {
"method": "POST",
"url": "/consent/non-existent-id/approve",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-l"
res: {
"statusCode": 400
}
responseTime: 0.20058298110961914
✓ tests/server.test.ts (16 tests) 1062ms
✓ REST Server > Unlock Rate Limiting > should return 429 after too many failed attempts 445ms
[19:52:13] INFO: incoming request
reqId: "req-m"
req: {
"method": "POST",
"url": "/consent/non-existent-id/deny",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-m"
res: {
"statusCode": 400
}
responseTime: 0.060208916664123535
[19:52:13] INFO: incoming request
reqId: "req-n"
req: {
"method": "POST",
"url": "/revoke/non-existent-agent",
"host": "localhost:80",
"remoteAddress": "127.0.0.1"
}
[19:52:13] INFO: request completed
reqId: "req-n"
res: {
"statusCode": 200
}
responseTime: 0.07862496376037598
Test Files 6 passed (6)
Tests 107 passed (107)
Start at 14:52:12
Duration 1.76s (transform 357ms, setup 113ms, collect 745ms, tests 1.10s, environment 0ms, prepare 478ms)