@blockrun/franklin 3.15.9 → 3.15.11
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.
- package/README.md +2 -0
- package/assets/franklin-vscode-banner.png +0 -0
- package/dist/agent/loop.js +33 -34
- package/dist/agent/secret-redact.d.ts +72 -0
- package/dist/agent/secret-redact.js +240 -0
- package/dist/agent/tool-guard.js +16 -2
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +74 -0
- package/dist/stats/audit.d.ts +6 -0
- package/dist/stats/audit.js +40 -0
- package/dist/stats/insights.d.ts +19 -0
- package/dist/stats/insights.js +23 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/modal.d.ts +66 -0
- package/dist/tools/modal.js +639 -0
- package/dist/wallet/reservation.d.ts +51 -0
- package/dist/wallet/reservation.js +105 -0
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WalletReservation — local accounting layer for concurrent paid tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Problem this solves: when N batch tools (ImageGen / VideoGen) run in
|
|
5
|
+
* parallel, each independently checks balance and dispatches its x402
|
|
6
|
+
* payment. With balance $0.20 and 6 calls × $0.04 each, all 6 see "$0.20
|
|
7
|
+
* available, $0.04 fits" and start; only 5 can actually settle on-chain,
|
|
8
|
+
* the rest fail mid-flight with insufficient-funds and the user sees
|
|
9
|
+
* partial completion with no preflight warning.
|
|
10
|
+
*
|
|
11
|
+
* The fix is *not* on-chain — x402 is fire-and-forget per-request, there's
|
|
12
|
+
* no real "hold" capability. Instead this is a per-process bookkeeping
|
|
13
|
+
* layer:
|
|
14
|
+
* 1. Tool calls hold(amount) before paying.
|
|
15
|
+
* 2. hold() refuses if (balance - sum(active reservations)) < amount.
|
|
16
|
+
* 3. After payment succeeds OR fails, tool calls release(token).
|
|
17
|
+
*
|
|
18
|
+
* Single-process JS guarantees the check-and-set is atomic (no real race),
|
|
19
|
+
* and balance is cached briefly so we don't hit the RPC for every hold.
|
|
20
|
+
*/
|
|
21
|
+
export interface ReservationToken {
|
|
22
|
+
id: string;
|
|
23
|
+
amountUsd: number;
|
|
24
|
+
}
|
|
25
|
+
declare class WalletReservationManager {
|
|
26
|
+
private reserved;
|
|
27
|
+
private cachedBalance;
|
|
28
|
+
private balanceFetchInflight;
|
|
29
|
+
private fetchBalance;
|
|
30
|
+
private totalReserved;
|
|
31
|
+
/**
|
|
32
|
+
* Try to reserve `amountUsd`. Returns a token on success, or null if
|
|
33
|
+
* insufficient (balance - already-reserved < amountUsd). Caller MUST
|
|
34
|
+
* release the token after the actual payment resolves, success or fail.
|
|
35
|
+
*/
|
|
36
|
+
hold(amountUsd: number): Promise<ReservationToken | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Release a hold. Idempotent — releasing the same token twice is a no-op.
|
|
39
|
+
* Invalidate the balance cache so the next hold sees up-to-date state.
|
|
40
|
+
*/
|
|
41
|
+
release(token: ReservationToken | string | null | undefined): void;
|
|
42
|
+
/** Force the next hold() to refetch balance from chain. */
|
|
43
|
+
invalidateBalance(): void;
|
|
44
|
+
/** Snapshot of current reservation state — diagnostic / testing only. */
|
|
45
|
+
snapshot(): {
|
|
46
|
+
count: number;
|
|
47
|
+
totalUsd: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export declare const walletReservation: WalletReservationManager;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WalletReservation — local accounting layer for concurrent paid tool calls.
|
|
3
|
+
*
|
|
4
|
+
* Problem this solves: when N batch tools (ImageGen / VideoGen) run in
|
|
5
|
+
* parallel, each independently checks balance and dispatches its x402
|
|
6
|
+
* payment. With balance $0.20 and 6 calls × $0.04 each, all 6 see "$0.20
|
|
7
|
+
* available, $0.04 fits" and start; only 5 can actually settle on-chain,
|
|
8
|
+
* the rest fail mid-flight with insufficient-funds and the user sees
|
|
9
|
+
* partial completion with no preflight warning.
|
|
10
|
+
*
|
|
11
|
+
* The fix is *not* on-chain — x402 is fire-and-forget per-request, there's
|
|
12
|
+
* no real "hold" capability. Instead this is a per-process bookkeeping
|
|
13
|
+
* layer:
|
|
14
|
+
* 1. Tool calls hold(amount) before paying.
|
|
15
|
+
* 2. hold() refuses if (balance - sum(active reservations)) < amount.
|
|
16
|
+
* 3. After payment succeeds OR fails, tool calls release(token).
|
|
17
|
+
*
|
|
18
|
+
* Single-process JS guarantees the check-and-set is atomic (no real race),
|
|
19
|
+
* and balance is cached briefly so we don't hit the RPC for every hold.
|
|
20
|
+
*/
|
|
21
|
+
import { setupAgentWallet, setupAgentSolanaWallet } from '@blockrun/llm';
|
|
22
|
+
import { loadChain } from '../config.js';
|
|
23
|
+
const BALANCE_CACHE_MS = 5_000;
|
|
24
|
+
class WalletReservationManager {
|
|
25
|
+
reserved = new Map();
|
|
26
|
+
cachedBalance = null;
|
|
27
|
+
balanceFetchInflight = null;
|
|
28
|
+
async fetchBalance() {
|
|
29
|
+
if (this.cachedBalance && Date.now() - this.cachedBalance.fetchedAt < BALANCE_CACHE_MS) {
|
|
30
|
+
return this.cachedBalance.value;
|
|
31
|
+
}
|
|
32
|
+
if (this.balanceFetchInflight)
|
|
33
|
+
return this.balanceFetchInflight;
|
|
34
|
+
const chain = loadChain();
|
|
35
|
+
this.balanceFetchInflight = (async () => {
|
|
36
|
+
try {
|
|
37
|
+
if (chain === 'solana') {
|
|
38
|
+
const client = await setupAgentSolanaWallet({ silent: true });
|
|
39
|
+
return await client.getBalance();
|
|
40
|
+
}
|
|
41
|
+
const client = setupAgentWallet({ silent: true });
|
|
42
|
+
return await client.getBalance();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// If balance fetch fails, return Infinity so reservations don't
|
|
46
|
+
// block — the actual payment will surface the real error. We'd
|
|
47
|
+
// rather under-protect than block all paid tools on RPC flakiness.
|
|
48
|
+
return Number.POSITIVE_INFINITY;
|
|
49
|
+
}
|
|
50
|
+
})()
|
|
51
|
+
.then((v) => {
|
|
52
|
+
this.cachedBalance = { value: v, fetchedAt: Date.now() };
|
|
53
|
+
this.balanceFetchInflight = null;
|
|
54
|
+
return v;
|
|
55
|
+
});
|
|
56
|
+
return this.balanceFetchInflight;
|
|
57
|
+
}
|
|
58
|
+
totalReserved() {
|
|
59
|
+
let sum = 0;
|
|
60
|
+
for (const v of this.reserved.values())
|
|
61
|
+
sum += v;
|
|
62
|
+
return sum;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Try to reserve `amountUsd`. Returns a token on success, or null if
|
|
66
|
+
* insufficient (balance - already-reserved < amountUsd). Caller MUST
|
|
67
|
+
* release the token after the actual payment resolves, success or fail.
|
|
68
|
+
*/
|
|
69
|
+
async hold(amountUsd) {
|
|
70
|
+
if (amountUsd <= 0) {
|
|
71
|
+
// Free / zero-cost calls don't need accounting.
|
|
72
|
+
return { id: `free-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, amountUsd: 0 };
|
|
73
|
+
}
|
|
74
|
+
const balance = await this.fetchBalance();
|
|
75
|
+
const available = balance - this.totalReserved();
|
|
76
|
+
if (available < amountUsd)
|
|
77
|
+
return null;
|
|
78
|
+
const id = `res-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
79
|
+
this.reserved.set(id, amountUsd);
|
|
80
|
+
return { id, amountUsd };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Release a hold. Idempotent — releasing the same token twice is a no-op.
|
|
84
|
+
* Invalidate the balance cache so the next hold sees up-to-date state.
|
|
85
|
+
*/
|
|
86
|
+
release(token) {
|
|
87
|
+
if (!token)
|
|
88
|
+
return;
|
|
89
|
+
const id = typeof token === 'string' ? token : token.id;
|
|
90
|
+
if (this.reserved.delete(id)) {
|
|
91
|
+
// A real payment may have just settled on-chain; force re-fetch
|
|
92
|
+
// next time so subsequent holds see the post-payment balance.
|
|
93
|
+
this.cachedBalance = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Force the next hold() to refetch balance from chain. */
|
|
97
|
+
invalidateBalance() {
|
|
98
|
+
this.cachedBalance = null;
|
|
99
|
+
}
|
|
100
|
+
/** Snapshot of current reservation state — diagnostic / testing only. */
|
|
101
|
+
snapshot() {
|
|
102
|
+
return { count: this.reserved.size, totalUsd: this.totalReserved() };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export const walletReservation = new WalletReservationManager();
|
package/package.json
CHANGED