@mysten-incubation/sui-agent-pass 0.1.8 → 0.1.10
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/deployments/mainnet.json +4 -2
- package/dist/chain.js +64 -0
- package/dist/describe.js +2 -0
- package/dist/domain.js +45 -15
- package/dist/plugin.js +68 -20
- package/docs/PLAYBOOK.md +24 -15
- package/package.json +1 -1
- package/skills/agent-pass/SKILL.md +24 -15
- package/web-dist/assets/{index-eefC1BJ8.js → index-B-mJDYLu.js} +62 -62
- package/web-dist/assets/{mount-BV2Q3os4.js → mount-Dgq2YumU.js} +1 -1
- package/web-dist/deployments/mainnet.json +4 -2
- package/web-dist/index.html +1 -1
package/deployments/mainnet.json
CHANGED
|
@@ -35,12 +35,14 @@
|
|
|
35
35
|
"protocols": {
|
|
36
36
|
"deepbook": {
|
|
37
37
|
"packageId": "0x2c8d603bc51326b8c13cef9dd07031a408a48dddb541963357661df5d3204809",
|
|
38
|
-
"publishedAt": "0x0e735f8c93a95722efd73521aca7a7652c0bb71ed1daf41b26dfd7d1ff71f748"
|
|
38
|
+
"publishedAt": "0x0e735f8c93a95722efd73521aca7a7652c0bb71ed1daf41b26dfd7d1ff71f748",
|
|
39
|
+
"indexerUrl": "https://deepbook-indexer.mainnet.mystenlabs.com"
|
|
39
40
|
},
|
|
40
41
|
"cetus": {
|
|
41
42
|
"packageId": "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb",
|
|
42
43
|
"publishedAt": "0x260693ec785a6e6c9d81d58c7d2ff72f1288ae0fa6a9725abe05a6478b11f084",
|
|
43
|
-
"configId": "0xdaa46292632c3c4d8f31f23ea0f9b36a28ff3677e9684980e4438403a67a3d8f"
|
|
44
|
+
"configId": "0xdaa46292632c3c4d8f31f23ea0f9b36a28ff3677e9684980e4438403a67a3d8f",
|
|
45
|
+
"apiUrl": "https://api-sui.cetus.zone"
|
|
44
46
|
},
|
|
45
47
|
"navi": {
|
|
46
48
|
"packageId": "0x66c91a8560cd64d73d93dd1ec7b61f3f21ad2f66553dd3d7038ca69255479bb7",
|
package/dist/chain.js
CHANGED
|
@@ -661,6 +661,70 @@ export async function withMarketFacts(client, d, needs) {
|
|
|
661
661
|
}
|
|
662
662
|
return { ...d, coins, pools };
|
|
663
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Pools on DeepBook and Cetus that trade `coin` (against `other` when given), largest Cetus TVL
|
|
666
|
+
* first. The venues' public listings only nominate candidates; each one is then read from chain
|
|
667
|
+
* by `withMarketFacts`, which drops anything that is not a pool of those coins. Returns the
|
|
668
|
+
* deployment extended with the pools and coins found.
|
|
669
|
+
*/
|
|
670
|
+
export async function findPools(client, d, coin, other) {
|
|
671
|
+
const want = normalizeStructTag(coin), with_ = other ? normalizeStructTag(other) : null;
|
|
672
|
+
const fits = (a, b) => {
|
|
673
|
+
const [x, y] = [normalizeStructTag(a), normalizeStructTag(b)];
|
|
674
|
+
return (x === want && (!with_ || y === with_)) || (y === want && (!with_ || x === with_));
|
|
675
|
+
};
|
|
676
|
+
const candidates = new Map();
|
|
677
|
+
for (const p of d.pools)
|
|
678
|
+
if (fits(p.coinA, p.coinB))
|
|
679
|
+
candidates.set(p.poolId, null);
|
|
680
|
+
const { deepbook, cetus } = d.protocols;
|
|
681
|
+
const listings = await Promise.all([
|
|
682
|
+
deepbook.indexerUrl
|
|
683
|
+
? fetch(`${deepbook.indexerUrl}/get_pools`, { signal: AbortSignal.timeout(8_000) })
|
|
684
|
+
.then((r) => r.json())
|
|
685
|
+
.then((all) => all
|
|
686
|
+
.filter((p) => fits(p.base_asset_id, p.quote_asset_id))
|
|
687
|
+
.map((p) => [p.pool_id, null]))
|
|
688
|
+
.catch(() => [])
|
|
689
|
+
: [],
|
|
690
|
+
cetus.apiUrl
|
|
691
|
+
? fetch(`${cetus.apiUrl}/v3/sui/clmm/stats_pools`, {
|
|
692
|
+
method: 'POST',
|
|
693
|
+
headers: { 'content-type': 'application/json' },
|
|
694
|
+
body: JSON.stringify({
|
|
695
|
+
filter: '',
|
|
696
|
+
sortBy: 'tvl',
|
|
697
|
+
sortOrder: 'desc',
|
|
698
|
+
limit: 20,
|
|
699
|
+
offset: 0,
|
|
700
|
+
coinTypes: [want],
|
|
701
|
+
}),
|
|
702
|
+
signal: AbortSignal.timeout(8_000),
|
|
703
|
+
})
|
|
704
|
+
.then((r) => r.json())
|
|
705
|
+
.then((r) => (r.data?.list ?? [])
|
|
706
|
+
.filter((p) => fits(p.coinA.coinType, p.coinB.coinType))
|
|
707
|
+
.map((p) => [p.pool, p.tvl ?? null]))
|
|
708
|
+
.catch(() => [])
|
|
709
|
+
: [],
|
|
710
|
+
]);
|
|
711
|
+
for (const [id, tvl] of listings.flat())
|
|
712
|
+
candidates.set(normalizeSuiAddress(id), tvl);
|
|
713
|
+
const ranked = [...candidates]
|
|
714
|
+
.sort(([, a], [, b]) => Number(b ?? 0) - Number(a ?? 0))
|
|
715
|
+
.slice(0, 10);
|
|
716
|
+
let extended = d;
|
|
717
|
+
const found = [];
|
|
718
|
+
for (const [id, tvl] of ranked) {
|
|
719
|
+
const next = await withMarketFacts(client, extended, { coins: [], pools: [id] }).catch(() => null);
|
|
720
|
+
const pool = next?.pools.find((p) => p.poolId === normalizeSuiAddress(id));
|
|
721
|
+
if (!next || !pool || !fits(pool.coinA, pool.coinB))
|
|
722
|
+
continue;
|
|
723
|
+
extended = next;
|
|
724
|
+
found.push({ pool, tvlUsd: tvl === null ? null : Number(tvl).toFixed(0) });
|
|
725
|
+
}
|
|
726
|
+
return { d: extended, found };
|
|
727
|
+
}
|
|
664
728
|
function structTagString(t) {
|
|
665
729
|
const params = t.typeParams.map((p) => (typeof p === 'string' ? p : structTagString(p)));
|
|
666
730
|
return `${t.address}::${t.module}::${t.name}${params.length ? `<${params.join(',')}>` : ''}`;
|
package/dist/describe.js
CHANGED
|
@@ -277,6 +277,8 @@ const normalizeType = (type) => type.replace(/^0x0*([0-9a-f]+)::/i, (_, a) => `$
|
|
|
277
277
|
/** The owner's rule behind an abort, from the codes the policy modules define. */
|
|
278
278
|
export function describeRefusal(d, error, recipient) {
|
|
279
279
|
const abort = error.MoveAbort;
|
|
280
|
+
if (error.message === 'InsufficientBalanceForWithdraw')
|
|
281
|
+
return "The owner's address balance does not hold enough of this coin right now. This is not one of the owner's rules: the action can run once the balance covers it (InsufficientBalanceForWithdraw)";
|
|
280
282
|
if (!abort?.location?.module)
|
|
281
283
|
return error.message;
|
|
282
284
|
const code = Number(abort.cleverError?.errorCode ?? abort.abortCode);
|
package/dist/domain.js
CHANGED
|
@@ -164,10 +164,20 @@ export const DeploymentSchema = z
|
|
|
164
164
|
packageId: address,
|
|
165
165
|
/** The latest upgrade, for calls made from a PTB; the original is version-gated. */
|
|
166
166
|
publishedAt: address,
|
|
167
|
+
/** DeepBook's public indexer, which lists every pool; used only to find candidates. */
|
|
168
|
+
indexerUrl: z.string().url().optional(),
|
|
167
169
|
})
|
|
168
170
|
.strict(),
|
|
169
171
|
/** `packageId` defines the pool type; `publishedAt` is the upgrade that answers calls. */
|
|
170
|
-
cetus: z
|
|
172
|
+
cetus: z
|
|
173
|
+
.object({
|
|
174
|
+
packageId: address,
|
|
175
|
+
publishedAt: address,
|
|
176
|
+
configId: address,
|
|
177
|
+
/** Cetus's public API, which lists pools by coin; used only to find candidates. */
|
|
178
|
+
apiUrl: z.string().url().optional(),
|
|
179
|
+
})
|
|
180
|
+
.strict(),
|
|
171
181
|
navi: z
|
|
172
182
|
.object({
|
|
173
183
|
/** The package that defines `account::AccountCap`, which the pass holds. */
|
|
@@ -399,25 +409,45 @@ export function drawable(setup) {
|
|
|
399
409
|
return Object.fromEntries(Object.entries(need).map(([c, v]) => [c, v.toString()]));
|
|
400
410
|
}
|
|
401
411
|
/**
|
|
402
|
-
* Allowances draw only from the owner's address balance, never from coin objects.
|
|
403
|
-
* authorization converts
|
|
404
|
-
*
|
|
405
|
-
*
|
|
412
|
+
* Allowances draw only from the owner's address balance, never from coin objects. The
|
|
413
|
+
* authorization converts coin objects into that balance, per coin, as far as the rules can draw
|
|
414
|
+
* (`drawable`) and the owner has them; the caps are limits, not money that must exist at signing.
|
|
415
|
+
* SUI keeps the stipend and the fee reserve in coin objects for the transaction itself. `mintable`
|
|
416
|
+
* is a demo coin the transaction mints instead, converted in full. Converted coins stay the
|
|
417
|
+
* owner's to spend.
|
|
418
|
+
*/
|
|
419
|
+
export function conversion(setup, held, mintable = null) {
|
|
420
|
+
const of = (coin) => held[normalizeStructTag(coin)];
|
|
421
|
+
return Object.fromEntries(Object.entries(drawable(setup)).map(([coin, want]) => {
|
|
422
|
+
const short = BigInt(want) - BigInt(of(coin)?.addressBalance ?? '0');
|
|
423
|
+
if (short <= 0n)
|
|
424
|
+
return [coin, '0'];
|
|
425
|
+
if (mintable && sameCoin(mintable, coin))
|
|
426
|
+
return [coin, short.toString()];
|
|
427
|
+
let spare = BigInt(of(coin)?.coinBalance ?? '0');
|
|
428
|
+
if (sameCoin(coin, SUI))
|
|
429
|
+
spare -= BigInt(setup.gasAmount) + OWNER_FEE_RESERVE;
|
|
430
|
+
return [coin, (spare <= 0n ? 0n : short < spare ? short : spare).toString()];
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* What the rules can draw beyond what the address balance will hold right after signing, per
|
|
435
|
+
* coin. Not an error: withdrawals work while the balance covers them, and swap proceeds land in
|
|
436
|
+
* it. Coins with nothing uncovered are left out.
|
|
406
437
|
*/
|
|
407
|
-
export function
|
|
408
|
-
const
|
|
409
|
-
return Object.fromEntries(Object.entries(drawable(setup)).
|
|
410
|
-
coin
|
|
411
|
-
|
|
412
|
-
|
|
438
|
+
export function uncovered(setup, held, mintable = null) {
|
|
439
|
+
const convert = conversion(setup, held, mintable);
|
|
440
|
+
return Object.fromEntries(Object.entries(drawable(setup)).flatMap(([coin, want]) => {
|
|
441
|
+
const covered = BigInt(held[normalizeStructTag(coin)]?.addressBalance ?? '0') + BigInt(convert[coin] ?? '0');
|
|
442
|
+
return covered >= BigInt(want) ? [] : [[coin, { drawable: want, covered: covered.toString() }]];
|
|
443
|
+
}));
|
|
413
444
|
}
|
|
414
445
|
/**
|
|
415
|
-
* SUI the owner must hold
|
|
416
|
-
*
|
|
417
|
-
* be a coin object; the authorization consolidates what it touches into the address balance.
|
|
446
|
+
* SUI the owner must hold to sign, address balance and coin objects together: only what this
|
|
447
|
+
* transaction moves, the agent's gas stipend, plus a fee reserve. The caps need nothing up front.
|
|
418
448
|
*/
|
|
419
449
|
export function requiredSui(setup) {
|
|
420
|
-
return (BigInt(
|
|
450
|
+
return (BigInt(setup.gasAmount) + OWNER_FEE_RESERVE).toString();
|
|
421
451
|
}
|
|
422
452
|
/** Set aside for the owner's own transaction fees when checking what they hold. */
|
|
423
453
|
export const OWNER_FEE_RESERVE = 50000000n;
|
package/dist/plugin.js
CHANGED
|
@@ -20,9 +20,9 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
|
|
|
20
20
|
import { Transaction } from '@mysten/sui/transactions';
|
|
21
21
|
import { fromBase64, normalizeSuiAddress } from '@mysten/sui/utils';
|
|
22
22
|
import { z } from 'zod';
|
|
23
|
-
import { clientFor, deepbookSizes, quoteSwap, readAllHoldings, readStatus, withMarketFacts, } from './chain.js';
|
|
23
|
+
import { clientFor, deepbookSizes, findPools, quoteSwap, readAllHoldings, readHoldings, readStatus, withMarketFacts, } from './chain.js';
|
|
24
24
|
import { describePolicy, describeSetup, renderRows } from './describe.js';
|
|
25
|
-
import { address, coinBySymbol, coinInfo, DAY_MS, defaultGasDaily, DeploymentSchema, display, isStable, limitUnits, poolsFor, recipientName, sameCoin, SetupSchema, showCoin, units, VenueSchema, } from './domain.js';
|
|
25
|
+
import { address, coinBySymbol, coinInfo, DAY_MS, defaultGasDaily, DeploymentSchema, display, isStable, limitUnits, poolsFor, recipientName, requiredSui, sameCoin, SetupSchema, showCoin, SUI, uncovered, units, VenueSchema, } from './domain.js';
|
|
26
26
|
import { passesByAgent } from './explorer.js';
|
|
27
27
|
import { passEnded, returnGasFloat } from './float.js';
|
|
28
28
|
import { failure, INSTRUCTIONS, registerDocs, result, serverInfo, sessionInfo, SIGN_ANNOTATIONS, SIGN_DESCRIPTION, } from './mcp.js';
|
|
@@ -233,7 +233,7 @@ const rulesInput = {
|
|
|
233
233
|
pools: z
|
|
234
234
|
.array(address)
|
|
235
235
|
.optional()
|
|
236
|
-
.describe('pool
|
|
236
|
+
.describe('pool ids from find_pools for a pair the network does not list; never ask the owner for one. Verified on chain'),
|
|
237
237
|
perTrade: decimal.describe('sold coin per trade'),
|
|
238
238
|
daily: decimal.describe('sold coin per UTC day'),
|
|
239
239
|
limitPrice: decimal.describe("the owner's price limit. With a stablecoin on one side it is that stablecoin per one of the other coin: the most paid when buying, the least accepted when selling. Otherwise it is bought coin per one sold coin, the least accepted. Required, above 0, and only the owner sets it: ask them, showing the pool's current price from list_passes"),
|
|
@@ -311,7 +311,7 @@ async function toSetup(client, d0, input) {
|
|
|
311
311
|
(!sw.pools || sw.pools.some((id) => normalizeSuiAddress(id) === p.poolId)));
|
|
312
312
|
if (!pools.length)
|
|
313
313
|
throw new Error(d.pools.length
|
|
314
|
-
? `No pool trades ${sw.sell} for ${sw.buy}${sw.venues ? ` on ${sw.venues.join(', ')}` : ''}${sw.pools ? ` among ${sw.pools.join(', ')}` : ''};
|
|
314
|
+
? `No pool trades ${sw.sell} for ${sw.buy}${sw.venues ? ` on ${sw.venues.join(', ')}` : ''}${sw.pools ? ` among ${sw.pools.join(', ')}` : ''}; look one up with find_pools and pass its id in pools`
|
|
315
315
|
: `Swaps are not offered on ${d.network}: it lists no pools. Propose a pass on a network that does, or leave swaps out`);
|
|
316
316
|
return {
|
|
317
317
|
coinIn,
|
|
@@ -405,6 +405,20 @@ export async function forgetSession(directory) {
|
|
|
405
405
|
await registry.all();
|
|
406
406
|
return registry.forget(basename(resolve(directory)));
|
|
407
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* What a pool pays for ten units of its non-stable coin (coinA when neither or both are
|
|
410
|
+
* stablecoins), quoted like a rule's price; ten clears DeepBook's minimum order.
|
|
411
|
+
*/
|
|
412
|
+
async function poolPrice(d, p) {
|
|
413
|
+
const flip = isStable(coinInfo(d, p.coinA).symbol) && !isStable(coinInfo(d, p.coinB).symbol);
|
|
414
|
+
const a = coinInfo(d, flip ? p.coinB : p.coinA), b = coinInfo(d, flip ? p.coinA : p.coinB);
|
|
415
|
+
const amountIn = (10n * 10n ** BigInt(a.decimals)).toString();
|
|
416
|
+
const q = await quoteSwap(clientFor(d), d, p, a.type, amountIn).catch(() => null);
|
|
417
|
+
if (!q || q.amountOut === '0')
|
|
418
|
+
return {};
|
|
419
|
+
const perUnit = (BigInt(q.amountOut) * 10n ** BigInt(a.decimals)) / BigInt(q.amountIn);
|
|
420
|
+
return { price: `${display(perUnit.toString(), b.decimals)} ${b.symbol} per ${a.symbol}` };
|
|
421
|
+
}
|
|
408
422
|
export function createPluginMcp(registry) {
|
|
409
423
|
const server = new McpServer({ name: 'agent-pass', version: '0.1.0' }, {
|
|
410
424
|
instructions: `${INSTRUCTIONS} This server also runs the owner flow: propose_authorization turns reviewed rules into a pass proposal and opens the owner's wallet page; session_info (with wait) reports when the owner has signed and the on-chain policies match; revoke_pass reopens the owner page for revocation. Show the owner the exact "rules" text returned by propose_authorization before they sign; never paraphrase amounts.`,
|
|
@@ -420,30 +434,18 @@ export function createPluginMcp(registry) {
|
|
|
420
434
|
annotations: { readOnlyHint: true },
|
|
421
435
|
}, async ({ owner }) => {
|
|
422
436
|
const path = registry.deploymentPath();
|
|
423
|
-
// What the pool pays for ten units of its non-stable coin (coinA when neither or both are
|
|
424
|
-
// stablecoins), quoted like a rule's price; ten clears DeepBook's minimum order.
|
|
425
|
-
const price = async (d, p) => {
|
|
426
|
-
const flip = isStable(coinInfo(d, p.coinA).symbol) && !isStable(coinInfo(d, p.coinB).symbol);
|
|
427
|
-
const a = coinInfo(d, flip ? p.coinB : p.coinA), b = coinInfo(d, flip ? p.coinA : p.coinB);
|
|
428
|
-
const amountIn = (10n * 10n ** BigInt(a.decimals)).toString();
|
|
429
|
-
const q = await quoteSwap(clientFor(d), d, p, a.type, amountIn).catch(() => null);
|
|
430
|
-
if (!q || q.amountOut === '0')
|
|
431
|
-
return {};
|
|
432
|
-
const perUnit = (BigInt(q.amountOut) * 10n ** BigInt(a.decimals)) / BigInt(q.amountIn);
|
|
433
|
-
return { price: `${display(perUnit.toString(), b.decimals)} ${b.symbol} per ${a.symbol}` };
|
|
434
|
-
};
|
|
435
437
|
const markets = async (d) => ({
|
|
436
438
|
network: d.network,
|
|
437
439
|
coins: d.coins,
|
|
438
440
|
pools: await Promise.all(d.pools.map(async (p) => ({
|
|
439
441
|
venue: p.venue,
|
|
440
442
|
pair: `${coinInfo(d, p.coinA).symbol}/${coinInfo(d, p.coinB).symbol}`,
|
|
441
|
-
...(await
|
|
443
|
+
...(await poolPrice(d, p)),
|
|
442
444
|
}))),
|
|
443
445
|
lending: Object.keys(d.protocols.navi.reserves).map((c) => coinInfo(d, c).symbol),
|
|
444
446
|
...(d.pools.length || Object.keys(d.protocols.navi.reserves).length
|
|
445
447
|
? {
|
|
446
|
-
otherCoins: 'These are the coins and pools this package lists, not all it can use. Any other coin the owner holds goes in a transfer or swap rule by its full type; a swap
|
|
448
|
+
otherCoins: 'These are the coins and pools this package lists, not all it can use. Any other coin the owner holds goes in a transfer or swap rule by its full type; for a swap, find_pools looks up the DeepBook and Cetus pools for it, and their ids go in pools. Never ask the owner for a pool id. The server reads the coin and the pool from chain. Lending stays with the listed coins.',
|
|
447
449
|
}
|
|
448
450
|
: {
|
|
449
451
|
note: 'Transfers and staking only: this network lists no pools and no NAVI reserves, so swap and lending rules are refused here.',
|
|
@@ -469,8 +471,40 @@ export function createPluginMcp(registry) {
|
|
|
469
471
|
networks: [...own, ...bundled.filter((b) => !own.some((o) => o.network === b.network))],
|
|
470
472
|
});
|
|
471
473
|
});
|
|
474
|
+
server.registerTool('find_pools', {
|
|
475
|
+
description: "Find the DeepBook and Cetus pools that trade a coin, optionally against one other coin, for a swap rule. Each result is read from chain and carries its venue, id, pair, both coin types, Cetus TVL in USD when known and the pool's current price. Use it instead of asking the owner for a pool; prefer the deepest pool, and when the coin is ambiguous show the owner their balance of it and this price so they can confirm it is the right token.",
|
|
476
|
+
inputSchema: {
|
|
477
|
+
coin: symbol.describe('the coin, by symbol when list_passes lists it, else by full type'),
|
|
478
|
+
with: symbol.optional().describe('the other coin of the pair, same form; any when omitted'),
|
|
479
|
+
network: z
|
|
480
|
+
.enum(['mainnet', 'testnet', 'devnet', 'localnet'])
|
|
481
|
+
.optional()
|
|
482
|
+
.describe('defaults to the network a new pass would use'),
|
|
483
|
+
},
|
|
484
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
485
|
+
}, async ({ coin, with: other, network }) => {
|
|
486
|
+
try {
|
|
487
|
+
const d0 = await registry.deployment(undefined, network);
|
|
488
|
+
const type = (s) => (/::/.test(s) ? s : coinBySymbol(d0, s).type);
|
|
489
|
+
const { d, found } = await findPools(clientFor(d0), d0, type(coin), other ? type(other) : undefined);
|
|
490
|
+
const priced = await Promise.all(found.map(async ({ pool, tvlUsd }) => ({
|
|
491
|
+
venue: pool.venue,
|
|
492
|
+
poolId: pool.poolId,
|
|
493
|
+
pair: `${coinInfo(d, pool.coinA).symbol}/${coinInfo(d, pool.coinB).symbol}`,
|
|
494
|
+
coinA: pool.coinA,
|
|
495
|
+
coinB: pool.coinB,
|
|
496
|
+
tvlUsd,
|
|
497
|
+
...(await poolPrice(d, pool)),
|
|
498
|
+
})));
|
|
499
|
+
// A pool that cannot price ten units holds too little to trade against.
|
|
500
|
+
return result({ network: d.network, pools: priced.filter((p) => 'price' in p) });
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
return failure(error, 'Pool lookup failed');
|
|
504
|
+
}
|
|
505
|
+
});
|
|
472
506
|
server.registerTool('propose_authorization', {
|
|
473
|
-
description: "Create a new pass proposal from rules in human units (coins by symbol when list_passes lists them for the network, else by full type, with pool ids for a pair the network list lacks; amounts as decimal strings) and open the owner's review page, where they connect Slush, check the same rules and sign one transaction. That transaction creates the pass, converts the owner's coins into their own address balance as far as the caps need, issues the allowances the agent will draw on, installs the policies and gives the new agent key its gas; the owner's coins never enter the pass. Returns the exact rules text to show the owner, the review URL and the session address. Nothing is on chain until the owner signs; poll session_info with wait afterwards.",
|
|
507
|
+
description: "Create a new pass proposal from rules in human units (coins by symbol when list_passes lists them for the network, else by full type, with pool ids from find_pools for a pair the network list lacks; amounts as decimal strings) and open the owner's review page, where they connect Slush, check the same rules and sign one transaction. That transaction creates the pass, converts the owner's coins into their own address balance as far as the caps need, issues the allowances the agent will draw on, installs the policies and gives the new agent key its gas; the owner's coins never enter the pass. Returns the exact rules text to show the owner, the review URL and the session address. Nothing is on chain until the owner signs; poll session_info with wait afterwards.",
|
|
474
508
|
inputSchema: {
|
|
475
509
|
name: z
|
|
476
510
|
.string()
|
|
@@ -495,9 +529,12 @@ export function createPluginMcp(registry) {
|
|
|
495
529
|
.string()
|
|
496
530
|
.optional()
|
|
497
531
|
.describe('path of a private deployment.json; overrides network'),
|
|
532
|
+
owner: address
|
|
533
|
+
.optional()
|
|
534
|
+
.describe("the owner's address, when they gave it: adds warnings where the caps reach past what they hold, or when they cannot sign yet"),
|
|
498
535
|
},
|
|
499
536
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
500
|
-
}, async ({ deployment, network, ...input }) => {
|
|
537
|
+
}, async ({ deployment, network, owner, ...input }) => {
|
|
501
538
|
try {
|
|
502
539
|
const d0 = await registry.deployment(deployment, network);
|
|
503
540
|
const { setup, d } = await toSetup(clientFor(d0), d0, input);
|
|
@@ -523,6 +560,17 @@ export function createPluginMcp(registry) {
|
|
|
523
560
|
warnings.push(`DeepBook takes its fee from the input and fills nothing under its minimum order, so a sale needs at least ${showCoin(d, sw.coinIn, floor)} and this pass allows ${showCoin(d, sw.coinIn, sw.perTrade)}: no sale could ever happen. Propose again with Cetus added to venues or a larger perTrade, unless the owner wants exactly this.`);
|
|
524
561
|
}
|
|
525
562
|
}
|
|
563
|
+
if (owner) {
|
|
564
|
+
const holdings = await readHoldings(clientFor(d), d, owner).catch(() => null);
|
|
565
|
+
if (holdings) {
|
|
566
|
+
const sui = holdings[SUI];
|
|
567
|
+
const total = BigInt(sui?.addressBalance ?? '0') + BigInt(sui?.coinBalance ?? '0');
|
|
568
|
+
if (total < BigInt(requiredSui(setup)))
|
|
569
|
+
warnings.push(`The owner holds ${showCoin(d, SUI, total.toString())} and signing needs ${showCoin(d, SUI, requiredSui(setup))} for the agent's gas stipend and the fee; they must add SUI before they can sign.`);
|
|
570
|
+
for (const [coin, { drawable: most, covered }] of Object.entries(uncovered(setup, holdings, d.demoFaucet?.coin ?? null)))
|
|
571
|
+
warnings.push(`The rules can draw up to ${showCoin(d, coin, most)} but the owner's address balance will hold ${showCoin(d, coin, covered)} after signing. Signing still works; each action runs only while the balance covers it, and swap proceeds that land there count. Tell the owner before they sign.`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
526
574
|
return result({
|
|
527
575
|
pass: session.id,
|
|
528
576
|
sessionAddress: session.store.state.sessionAddress,
|
package/docs/PLAYBOOK.md
CHANGED
|
@@ -54,14 +54,17 @@ pool), so list Cetus as well when trades will be small. A network may list no po
|
|
|
54
54
|
reserves; `list_passes` says so, and swap or lending rules are refused there.
|
|
55
55
|
|
|
56
56
|
**Coins and pools.** The coins and pools `list_passes` shows for a network are the ones this
|
|
57
|
-
package lists, not the only ones it can use. Name a listed coin by symbol
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
package lists, not the only ones it can use. Name a listed coin by symbol, and any other coin the
|
|
58
|
+
owner holds by its full type. To find its type and balance, ask the owner for their address and
|
|
59
|
+
call `list_passes` with `owner`: it adds every coin that address holds per network. For a swap,
|
|
60
|
+
find the pool with `find_pools`, which looks up DeepBook and Cetus, reads each pool from chain and
|
|
61
|
+
quotes its current price; pick the deepest one for the pair and pass its id in `pools`. Never ask
|
|
62
|
+
the owner for a pool id. When you are unsure you have the right token (several coins share a
|
|
63
|
+
symbol, or the owner named it loosely), show the owner their balance of it and its current price
|
|
64
|
+
and ask them to confirm. The server and the review page read a coin's symbol and decimals and a
|
|
65
|
+
pool's venue from chain, never from you, and the page shows an unlisted coin with its type so
|
|
66
|
+
the owner can tell a look-alike from the real one. Lending is limited to the coins the network
|
|
67
|
+
lists as NAVI reserves.
|
|
65
68
|
|
|
66
69
|
**Lifetime caps.** `caps` gives, per coin, the total the key may ever withdraw, for exactly the
|
|
67
70
|
coins the rules spend (staking spends SUI). A daily limit may not exceed its coin's cap. Size
|
|
@@ -79,19 +82,25 @@ ask for something like "restake with a better validator".
|
|
|
79
82
|
to the agent key, outside the rules. `gasCapSui` (default 0.1) is a separate allowance the key
|
|
80
83
|
draws on to top up its gas, and `gasDailySui` (default a fifth of `gasCapSui`) its daily limit.
|
|
81
84
|
|
|
82
|
-
**What the owner needs.**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
**What the owner needs.** Signing moves only the agent's gas stipend and pays the fee, so that is
|
|
86
|
+
all the owner must hold, in SUI. The caps are limits on what the rules may draw later, not money
|
|
87
|
+
needed now. Allowances draw only from the owner's address balance, never from coin objects; the
|
|
88
|
+
signature converts the owner's coin objects into that balance as far as each cap needs and they
|
|
89
|
+
have them, and the converted coins stay theirs. When a cap reaches past what the owner holds,
|
|
90
|
+
signing still works: each action runs while the balance covers it, and swap proceeds landing
|
|
91
|
+
there count right away, which is how a chain like TAKE to SUI to USDC funds its later legs. Pass
|
|
92
|
+
`owner` to `propose_authorization` when you know the address, and tell the owner about any
|
|
93
|
+
`warnings` it returns before they sign. An action refused because the balance was short is not
|
|
94
|
+
one of the owner's rules; say so.
|
|
86
95
|
|
|
87
96
|
Before you call anything, show the owner the rules you intend to propose as one short list and
|
|
88
97
|
ask for a yes, unless they already stated every rule explicitly.
|
|
89
98
|
|
|
90
99
|
## 2. Propose the pass and wait for the signature
|
|
91
100
|
|
|
92
|
-
`propose_authorization`, `propose_change`, `list_passes`, `revoke_pass` and
|
|
93
|
-
the stdio server (`agent-pass mcp`), which keeps every session on the
|
|
94
|
-
signer has only `session_info` and `sign_transaction`.
|
|
101
|
+
`propose_authorization`, `propose_change`, `list_passes`, `find_pools`, `revoke_pass` and
|
|
102
|
+
`forget_pass` live on the stdio server (`agent-pass mcp`), which keeps every session on the
|
|
103
|
+
machine. A per-session HTTP signer has only `session_info` and `sign_transaction`.
|
|
95
104
|
|
|
96
105
|
The pass goes on the network the owner names (`network`, one of `list_passes.networks`), else on
|
|
97
106
|
the first network listed. A private publish of one's own takes a `deployment` path instead.
|
package/package.json
CHANGED
|
@@ -59,14 +59,17 @@ pool), so list Cetus as well when trades will be small. A network may list no po
|
|
|
59
59
|
reserves; `list_passes` says so, and swap or lending rules are refused there.
|
|
60
60
|
|
|
61
61
|
**Coins and pools.** The coins and pools `list_passes` shows for a network are the ones this
|
|
62
|
-
package lists, not the only ones it can use. Name a listed coin by symbol
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
package lists, not the only ones it can use. Name a listed coin by symbol, and any other coin the
|
|
63
|
+
owner holds by its full type. To find its type and balance, ask the owner for their address and
|
|
64
|
+
call `list_passes` with `owner`: it adds every coin that address holds per network. For a swap,
|
|
65
|
+
find the pool with `find_pools`, which looks up DeepBook and Cetus, reads each pool from chain and
|
|
66
|
+
quotes its current price; pick the deepest one for the pair and pass its id in `pools`. Never ask
|
|
67
|
+
the owner for a pool id. When you are unsure you have the right token (several coins share a
|
|
68
|
+
symbol, or the owner named it loosely), show the owner their balance of it and its current price
|
|
69
|
+
and ask them to confirm. The server and the review page read a coin's symbol and decimals and a
|
|
70
|
+
pool's venue from chain, never from you, and the page shows an unlisted coin with its type so
|
|
71
|
+
the owner can tell a look-alike from the real one. Lending is limited to the coins the network
|
|
72
|
+
lists as NAVI reserves.
|
|
70
73
|
|
|
71
74
|
**Lifetime caps.** `caps` gives, per coin, the total the key may ever withdraw, for exactly the
|
|
72
75
|
coins the rules spend (staking spends SUI). A daily limit may not exceed its coin's cap. Size
|
|
@@ -84,19 +87,25 @@ ask for something like "restake with a better validator".
|
|
|
84
87
|
to the agent key, outside the rules. `gasCapSui` (default 0.1) is a separate allowance the key
|
|
85
88
|
draws on to top up its gas, and `gasDailySui` (default a fifth of `gasCapSui`) its daily limit.
|
|
86
89
|
|
|
87
|
-
**What the owner needs.**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
**What the owner needs.** Signing moves only the agent's gas stipend and pays the fee, so that is
|
|
91
|
+
all the owner must hold, in SUI. The caps are limits on what the rules may draw later, not money
|
|
92
|
+
needed now. Allowances draw only from the owner's address balance, never from coin objects; the
|
|
93
|
+
signature converts the owner's coin objects into that balance as far as each cap needs and they
|
|
94
|
+
have them, and the converted coins stay theirs. When a cap reaches past what the owner holds,
|
|
95
|
+
signing still works: each action runs while the balance covers it, and swap proceeds landing
|
|
96
|
+
there count right away, which is how a chain like TAKE to SUI to USDC funds its later legs. Pass
|
|
97
|
+
`owner` to `propose_authorization` when you know the address, and tell the owner about any
|
|
98
|
+
`warnings` it returns before they sign. An action refused because the balance was short is not
|
|
99
|
+
one of the owner's rules; say so.
|
|
91
100
|
|
|
92
101
|
Before you call anything, show the owner the rules you intend to propose as one short list and
|
|
93
102
|
ask for a yes, unless they already stated every rule explicitly.
|
|
94
103
|
|
|
95
104
|
## 2. Propose the pass and wait for the signature
|
|
96
105
|
|
|
97
|
-
`propose_authorization`, `propose_change`, `list_passes`, `revoke_pass` and
|
|
98
|
-
the stdio server (`agent-pass mcp`), which keeps every session on the
|
|
99
|
-
signer has only `session_info` and `sign_transaction`.
|
|
106
|
+
`propose_authorization`, `propose_change`, `list_passes`, `find_pools`, `revoke_pass` and
|
|
107
|
+
`forget_pass` live on the stdio server (`agent-pass mcp`), which keeps every session on the
|
|
108
|
+
machine. A per-session HTTP signer has only `session_info` and `sign_transaction`.
|
|
100
109
|
|
|
101
110
|
The pass goes on the network the owner names (`network`, one of `list_passes.networks`), else on
|
|
102
111
|
the first network listed. A private publish of one's own takes a `deployment` path instead.
|