@mysten-incubation/sui-agent-pass 0.1.3 → 0.1.5

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 CHANGED
@@ -334,11 +334,8 @@ brand` and `npx playwright install chromium`.
334
334
  - [Agent transaction guide](docs/AGENT.md): complete interface and agent-side examples.
335
335
  - [Architecture and boundaries](docs/ARCHITECTURE.md).
336
336
  - [Status: implemented and todo](docs/STATUS.md).
337
- - [Verification record](docs/VERIFICATION.md) and [generated chain evidence](docs/evidence/localnet.json),
338
- including the explorer's [pass page](docs/evidence/explorer-pass.png) and
339
- [booth view](docs/evidence/explorer-booth.png).
340
- - [Real Claude execution evidence](docs/evidence/claude.json): Claude Code with the plugin,
341
- from prompt to signed pass to on-chain actions on a localnet.
337
+ - [Verification record](docs/VERIFICATION.md): dated runs with their digests. The test scripts
338
+ write their reports and screenshots to `.local/evidence/`, which stays out of git.
342
339
 
343
340
  ## Checks
344
341
 
package/dist/describe.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { bcs } from '@mysten/sui/bcs';
9
9
  import { fromBase64, normalizeSuiAddress } from '@mysten/sui/utils';
10
- import { priceDisplay, sameCoin, showCoin, SUI, } from './domain.js';
10
+ import { limitDisplay, priceTerms, sameCoin, showCoin, SUI, } from './domain.js';
11
11
  const healthFactor = (ray) => (Number(BigInt(ray) / 10n ** 25n) / 100).toFixed(2);
12
12
  /** The reviewed rules, or the installed policies once `live` is read from chain. */
13
13
  export function describePolicy(d, p, live = null) {
@@ -34,32 +34,37 @@ export function describePolicy(d, p, live = null) {
34
34
  for (const t of p.transfers) {
35
35
  const l = live?.transfers.find((x) => sameCoin(x.coinType, t.coin));
36
36
  rows.push({
37
+ starts: true,
37
38
  label: `Pay ${symbol(t.coin)} to`,
38
39
  value: (l?.recipients ?? t.recipients).map((r) => `${r.name} → ${r.address}`).join('\n'),
39
40
  }, {
40
- label: `${symbol(t.coin)} per transfer · per UTC day`,
41
- value: `${show(t.coin, l?.perTransfer ?? t.perTransfer)} · ${show(t.coin, l?.daily ?? t.daily)}`,
41
+ label: `Max ${symbol(t.coin)} per transfer / per UTC day`,
42
+ value: `${show(t.coin, l?.perTransfer ?? t.perTransfer)} / ${show(t.coin, l?.daily ?? t.daily)}`,
42
43
  });
43
44
  }
44
45
  for (const sw of p.swaps) {
45
46
  const l = live?.swaps.find((x) => sameCoin(x.coinIn, sw.coinIn) && sameCoin(x.coinOut, sw.coinOut));
46
- const minPrice = l?.minPrice ?? sw.minPrice;
47
+ const minPrice = l?.minPrice ?? sw.minPrice, terms = priceTerms(d, sw.coinIn, sw.coinOut);
47
48
  rows.push({
48
- label: `Sell ${symbol(sw.coinIn)} for ${symbol(sw.coinOut)} on`,
49
+ starts: true,
50
+ label: terms.limit === 'max'
51
+ ? `Buy ${symbol(sw.coinOut)} with ${symbol(sw.coinIn)} on`
52
+ : `Sell ${symbol(sw.coinIn)} for ${symbol(sw.coinOut)} on`,
49
53
  value: (l?.venues ?? sw.pools).map(poolName).join(', '),
50
54
  }, {
51
- label: `${symbol(sw.coinIn)} per trade · per UTC day`,
52
- value: `${show(sw.coinIn, l?.perTrade ?? sw.perTrade)} · ${show(sw.coinIn, l?.daily ?? sw.daily)}`,
55
+ label: `Max ${symbol(sw.coinIn)} ${terms.limit === 'max' ? 'spent' : 'sold'} per trade / per UTC day`,
56
+ value: `${show(sw.coinIn, l?.perTrade ?? sw.perTrade)} / ${show(sw.coinIn, l?.daily ?? sw.daily)}`,
53
57
  }, {
54
- label: 'Lowest price accepted',
55
- value: `${priceDisplay(d, sw.coinIn, sw.coinOut, minPrice)} ${symbol(sw.coinOut)} per ${symbol(sw.coinIn)}`,
58
+ label: terms.limit === 'max' ? 'Highest price paid' : 'Lowest price accepted',
59
+ value: `${limitDisplay(d, sw.coinIn, sw.coinOut, minPrice)} ${symbol(terms.quote)} per ${symbol(terms.base)}`,
56
60
  });
57
61
  }
58
62
  for (const le of p.lending) {
59
63
  const l = live?.lending.find((x) => sameCoin(x.coinType, le.coin));
60
64
  rows.push({
61
- label: `NAVI ${le.allowBorrow ? 'borrowing' : 'collateral'} in ${symbol(le.coin)}, per action · per UTC day`,
62
- value: `${show(le.coin, l?.perAction ?? le.perAction)} · ${show(le.coin, l?.daily ?? le.daily)}`,
65
+ starts: true,
66
+ label: `Max NAVI ${le.allowBorrow ? 'borrowing' : 'collateral'} in ${symbol(le.coin)} per action / per UTC day`,
67
+ value: `${show(le.coin, l?.perAction ?? le.perAction)} / ${show(le.coin, l?.daily ?? le.daily)}`,
63
68
  });
64
69
  const floor = l ? healthFactor(l.minHealthFactor) : le.minHealthFactor?.toFixed(2);
65
70
  if (floor && floor !== '0.00')
@@ -69,11 +74,12 @@ export function describePolicy(d, p, live = null) {
69
74
  const l = live?.staking[0];
70
75
  const validators = l?.validators ?? p.staking.validators;
71
76
  rows.push({
77
+ starts: true,
72
78
  label: 'Stake SUI with',
73
79
  value: validators.length ? validators.join('\n') : 'Any active validator',
74
80
  }, {
75
- label: 'SUI per stake · per UTC day',
76
- value: `${show(SUI, l?.perStake ?? p.staking.perStake)} · ${show(SUI, l?.daily ?? p.staking.daily)}`,
81
+ label: 'Max SUI per stake / per UTC day',
82
+ value: `${show(SUI, l?.perStake ?? p.staking.perStake)} / ${show(SUI, l?.daily ?? p.staking.daily)}`,
77
83
  });
78
84
  }
79
85
  return rows;
@@ -88,6 +94,7 @@ export function describeSetup(d, setup) {
88
94
  return [
89
95
  ...describePolicy(d, setup.policy),
90
96
  {
97
+ starts: true,
91
98
  label: 'Authorization ends',
92
99
  value: `${utc.format(setup.expiresMs)} UTC (${setup.ttlMs / 86_400_000} ${setup.ttlMs === 86_400_000 ? 'day' : 'days'}); you can revoke earlier`,
93
100
  },
@@ -95,11 +102,11 @@ export function describeSetup(d, setup) {
95
102
  label: 'Agent may ever withdraw (lifetime caps)',
96
103
  value: Object.entries(setup.allowances)
97
104
  .map(([coin, cap]) => showCoin(d, coin, cap))
98
- .join(' · '),
105
+ .join(', '),
99
106
  },
100
107
  {
101
- label: 'Gas allowance, lifetime · per UTC day',
102
- value: `${showCoin(d, SUI, setup.gasCap)} · ${showCoin(d, SUI, setup.gasDaily)}`,
108
+ label: 'Max gas allowance, lifetime / per UTC day',
109
+ value: `${showCoin(d, SUI, setup.gasCap)} / ${showCoin(d, SUI, setup.gasDaily)}`,
103
110
  },
104
111
  {
105
112
  label: 'Agent gas stipend (outside the rules)',
@@ -107,7 +114,9 @@ export function describeSetup(d, setup) {
107
114
  },
108
115
  ];
109
116
  }
110
- export const renderRows = (rows) => rows.map(({ label, value }) => `- ${label}: ${value.replaceAll('\n', '; ')}`).join('\n');
117
+ export const renderRows = (rows) => rows
118
+ .map(({ label, value, starts }, i) => `${starts && i > 0 ? '\n' : ''}- ${label}: ${value.replaceAll('\n', '; ')}`)
119
+ .join('\n');
111
120
  const OWNER_ONLY = new Set([
112
121
  'deposit_object',
113
122
  'withdraw_object',
@@ -317,7 +326,7 @@ export function describeRefusal(d, error, recipient) {
317
326
  0: 'Not a pool you listed',
318
327
  1: 'Over your per-trade limit',
319
328
  3: "Below the agent's own minimum output",
320
- 4: 'Below your price floor',
329
+ 4: 'Worse than your price limit',
321
330
  8: 'The venue filled nothing (order under its minimum size)',
322
331
  },
323
332
  lending_policy: {
package/dist/domain.js CHANGED
@@ -349,6 +349,36 @@ export function priceDisplay(d, coinIn, coinOut, minPrice) {
349
349
  ? display(minPrice, shift)
350
350
  : (BigInt(minPrice) * 10n ** BigInt(-shift)).toString();
351
351
  }
352
+ /** Dollar stablecoins by symbol: USDC, USDT, USDY, USDsui and the like. */
353
+ export const isStable = (symbol) => /^(USD|AUSD$|FDUSD$|BUCK$)/i.test(symbol);
354
+ /**
355
+ * How a swap rule's price reads to a person. When exactly one side is a stablecoin the price is
356
+ * in that stablecoin per one of the other coin, so a buy reads as the most paid (the policy's
357
+ * floor inverted) and a sale as the least accepted. Otherwise it is `coinOut` per one `coinIn`.
358
+ */
359
+ export function priceTerms(d, coinIn, coinOut) {
360
+ const stableIn = isStable(coinInfo(d, coinIn).symbol), stableOut = isStable(coinInfo(d, coinOut).symbol);
361
+ return stableIn && !stableOut
362
+ ? { base: coinOut, quote: coinIn, limit: 'max' }
363
+ : { base: coinIn, quote: coinOut, limit: 'min' };
364
+ }
365
+ /** The policy floor for a person's price in `priceTerms`; a buy's maximum rounds the floor up. */
366
+ export function limitUnits(d, coinIn, coinOut, price) {
367
+ if (priceTerms(d, coinIn, coinOut).limit === 'min')
368
+ return priceUnits(d, coinIn, coinOut, price);
369
+ const paid = BigInt(units(price, coinInfo(d, coinIn).decimals));
370
+ if (paid === 0n)
371
+ throw new Error('A price must be above 0');
372
+ const scale = 10n ** BigInt(coinInfo(d, coinOut).decimals) * 10n ** 9n;
373
+ return ((scale + paid - 1n) / paid).toString();
374
+ }
375
+ /** A policy floor as a person reads it, in `priceTerms`, without the coin names. */
376
+ export function limitDisplay(d, coinIn, coinOut, minPrice) {
377
+ if (priceTerms(d, coinIn, coinOut).limit === 'min')
378
+ return priceDisplay(d, coinIn, coinOut, minPrice);
379
+ const floor = BigInt(minPrice), scale = 10n ** BigInt(coinInfo(d, coinOut).decimals) * 10n ** 9n;
380
+ return display(((scale * 2n + floor) / (floor * 2n)).toString(), coinInfo(d, coinIn).decimals);
381
+ }
352
382
  /** Health factors are entered with two decimals and stored on chain in ray units. */
353
383
  export const healthFactorRay = (hf) => hf === null ? '0' : ((BigInt(units(hf.toFixed(2), 2)) * RAY) / 100n).toString();
354
384
  /** Gas top-ups per UTC day when the owner names none: a fifth of the lifetime cap, so a runaway
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, readStatus, withMarketFacts } from './chain.js';
23
+ import { clientFor, deepbookSizes, quoteSwap, readAllHoldings, readStatus, withMarketFacts, } from './chain.js';
24
24
  import { describePolicy, describeSetup, renderRows } from './describe.js';
25
- import { address, coinBySymbol, coinInfo, DAY_MS, defaultGasDaily, DeploymentSchema, display, poolsFor, priceUnits, recipientName, sameCoin, SetupSchema, showCoin, units, VenueSchema, } from './domain.js';
25
+ import { address, coinBySymbol, coinInfo, DAY_MS, defaultGasDaily, DeploymentSchema, display, isStable, limitUnits, poolsFor, recipientName, sameCoin, SetupSchema, showCoin, 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, sessionInfo, SIGN_ANNOTATIONS, SIGN_DESCRIPTION, } from './mcp.js';
@@ -236,7 +236,7 @@ const rulesInput = {
236
236
  .describe('pool object ids for a pair the deployment does not list (a DeepBook or Cetus pool holding exactly these two coins); verified on chain'),
237
237
  perTrade: decimal.describe('sold coin per trade'),
238
238
  daily: decimal.describe('sold coin per UTC day'),
239
- minPrice: decimal.describe("lowest price accepted, in bought coin per one sold coin; required, above 0. When the owner names no floor, 1% under the pool's current price (list_passes shows it) and say so"),
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"),
240
240
  })
241
241
  .strict())
242
242
  .default([])
@@ -319,7 +319,7 @@ async function toSetup(client, d0, input) {
319
319
  pools: pools.map((p) => p.poolId),
320
320
  perTrade: inUnits(sw.sell, sw.perTrade),
321
321
  daily: inUnits(sw.sell, sw.daily),
322
- minPrice: priceUnits(d, coinIn, coinOut, sw.minPrice),
322
+ minPrice: limitUnits(d, coinIn, coinOut, sw.limitPrice),
323
323
  };
324
324
  }),
325
325
  lending: input.lending.map((l) => {
@@ -411,17 +411,22 @@ export function createPluginMcp(registry) {
411
411
  });
412
412
  registerDocs(server);
413
413
  server.registerTool('list_passes', {
414
- description: 'List the pass sessions on this machine (id, name, network, whether the owner has authorized it on chain) and the networks a new pass can live on, each with the coins and pools its rules may name.',
415
- inputSchema: {},
414
+ description: 'List the pass sessions on this machine (id, name, network, whether the owner has authorized it on chain) and the networks a new pass can live on, each with its listed coins and pools and how to name others. With owner, also every coin that address holds on each network, so rules can be sized from real balances before any pass exists.',
415
+ inputSchema: {
416
+ owner: address
417
+ .optional()
418
+ .describe("the owner's address, when they gave it; adds ownerCoins per network"),
419
+ },
416
420
  annotations: { readOnlyHint: true },
417
- }, async () => {
421
+ }, async ({ owner }) => {
418
422
  const path = registry.deploymentPath();
419
- // The pool's own price for ten units of coinA, so a floor can be sized from the market
420
- // before any pass exists; ten clears DeepBook's minimum order where one may not.
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.
421
425
  const price = async (d, p) => {
422
- const a = coinInfo(d, p.coinA), b = coinInfo(d, p.coinB);
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);
423
428
  const amountIn = (10n * 10n ** BigInt(a.decimals)).toString();
424
- const q = await quoteSwap(clientFor(d), d, p, p.coinA, amountIn).catch(() => null);
429
+ const q = await quoteSwap(clientFor(d), d, p, a.type, amountIn).catch(() => null);
425
430
  if (!q || q.amountOut === '0')
426
431
  return {};
427
432
  const perUnit = (BigInt(q.amountOut) * 10n ** BigInt(a.decimals)) / BigInt(q.amountIn);
@@ -437,10 +442,15 @@ export function createPluginMcp(registry) {
437
442
  }))),
438
443
  lending: Object.keys(d.protocols.navi.reserves).map((c) => coinInfo(d, c).symbol),
439
444
  ...(d.pools.length || Object.keys(d.protocols.navi.reserves).length
440
- ? {}
445
+ ? {
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 of it also needs, in pools, the id of a DeepBook or Cetus pool holding exactly that pair. The server reads the coin and the pool from chain. Lending stays with the listed coins.',
447
+ }
441
448
  : {
442
449
  note: 'Transfers and staking only: this network lists no pools and no NAVI reserves, so swap and lending rules are refused here.',
443
450
  }),
451
+ ...(owner
452
+ ? { ownerCoins: await readAllHoldings(clientFor(d), d, owner).catch(() => null) }
453
+ : {}),
444
454
  });
445
455
  const own = (await exists(path)) ? [await markets(await registry.deployment())] : [];
446
456
  const bundled = await Promise.all((await registry.bundledNetworks()).map(async (n) => markets(await registry.bundled(n))));