@mysten-incubation/sui-agent-pass 0.1.4 → 0.1.6

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
@@ -61,7 +61,11 @@ npm install -g @mysten-incubation/sui-agent-pass
61
61
  agent-pass connect # pick the agents on this machine; nothing stays running
62
62
  ```
63
63
 
64
- That is the only command. Then open the agent and tell it what to do with your money. It
64
+ That is the only command. For each agent you pick it registers the MCP server and links the
65
+ `agent-pass` skill, the playbook built into the package, into the agent's skills folder (Claude
66
+ Code, Codex, Cursor and Hermes), so the agent has the tools and the guidance for them. A link
67
+ follows upgrades; rerun `connect` if you installed through `npx`. Then open the agent and tell
68
+ it what to do with your money. It
65
69
  proposes a pass (`propose_authorization`) and opens the review page with the proposal in its URL.
66
70
  Connect a wallet, check the rules the agent quoted to you, and sign once; the session key stays
67
71
  local, and the agent sees your signature land on chain and acts within the rules. Claude Code
@@ -334,11 +338,8 @@ brand` and `npx playwright install chromium`.
334
338
  - [Agent transaction guide](docs/AGENT.md): complete interface and agent-side examples.
335
339
  - [Architecture and boundaries](docs/ARCHITECTURE.md).
336
340
  - [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.
341
+ - [Verification record](docs/VERIFICATION.md): dated runs with their digests. The test scripts
342
+ write their reports and screenshots to `.local/evidence/`, which stays out of git.
342
343
 
343
344
  ## Checks
344
345
 
package/dist/cli.js CHANGED
@@ -3,9 +3,9 @@
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  import { spawn } from 'node:child_process';
5
5
  import { realpathSync } from 'node:fs';
6
- import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
6
+ import { access, cp, lstat, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
7
7
  import { homedir } from 'node:os';
8
- import { dirname, join, resolve } from 'node:path';
8
+ import { dirname, join, resolve, sep } from 'node:path';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { select } from '@inquirer/prompts';
11
11
  import { Command } from 'commander';
@@ -174,6 +174,45 @@ async function registerClient(client, http) {
174
174
  console.log(JSON.stringify({ mcpServers: { 'agent-pass': http ?? stdio } }, null, 2));
175
175
  }
176
176
  }
177
+ /** Where each agent loads personal skills from, under the home directory. */
178
+ const SKILL_DIRS = {
179
+ 'claude-code': '.claude/skills',
180
+ codex: '.codex/skills',
181
+ cursor: '.cursor/skills',
182
+ hermes: '.hermes/skills',
183
+ };
184
+ /**
185
+ * Puts the agent-pass skill, the playbook built into this package, where the client loads
186
+ * skills. A link follows upgrades of the package; an npx cache is temporary, so it gets a copy.
187
+ */
188
+ async function installSkill(client) {
189
+ const source = fileURLToPath(new URL('../skills/agent-pass', import.meta.url));
190
+ if (!(await access(join(source, 'SKILL.md')).then(() => true, () => false)))
191
+ throw new Error(`The skill is missing from ${source}; run npm run build`);
192
+ const dir = SKILL_DIRS[client];
193
+ if (!dir) {
194
+ console.log(client === 'claude-desktop'
195
+ ? `Claude Desktop takes skills from its own settings; add ${join(source, 'SKILL.md')} there to give it the playbook.`
196
+ : `If the agent loads SKILL.md skills, link ${source} into its skills folder to give it the playbook.`);
197
+ return;
198
+ }
199
+ const target = join(homedir(), dir, 'agent-pass');
200
+ const existing = await lstat(target).catch(() => null);
201
+ if (existing && !existing.isSymbolicLink()) {
202
+ const ours = await readFile(join(target, 'SKILL.md'), 'utf8').then((t) => /^name: agent-pass$/m.test(t), () => false);
203
+ if (!ours)
204
+ throw new Error(`${target} exists and is not the agent-pass skill; move it away first`);
205
+ }
206
+ if (existing)
207
+ await rm(target, { recursive: true, force: true });
208
+ await mkdir(dirname(target), { recursive: true });
209
+ const temporary = source.includes(`${sep}_npx${sep}`);
210
+ if (temporary)
211
+ await cp(source, target, { recursive: true });
212
+ else
213
+ await symlink(source, target, 'dir');
214
+ console.log(`${temporary ? 'Copied' : 'Linked'} the agent-pass skill into ${target}.`);
215
+ }
177
216
  /** The agents on this machine, as far as their usual files show; nothing is preselected. */
178
217
  async function chooseClients() {
179
218
  const home = homedir();
@@ -234,10 +273,12 @@ program
234
273
  http = { url: mcpUrl, headers: { Authorization: `Bearer ${await store.mcpToken()}` } };
235
274
  }
236
275
  const clients = o.client ? [o.client] : await chooseClients();
237
- for (const client of clients)
276
+ for (const client of clients) {
238
277
  await registerClient(client, http);
278
+ await installSkill(client);
279
+ }
239
280
  if (!http)
240
- console.log('\nDone. Open the agent and tell it what to do with your money; it proposes the pass, you review and sign in your wallet, and it acts within the rules. Restart the agent after upgrading this package so it loads the new server.');
281
+ console.log('\nDone. Open the agent and tell it what to do with your money; it proposes the pass, you review and sign in your wallet, and it acts within the rules. Restart the agent after upgrading this package so it loads the new server and skill.');
241
282
  });
242
283
  // Run only when this file is the entry: directly, through tsx in development, or through the
243
284
  // package's `agent-pass` bin, which is a symlink to it.
package/dist/plugin.js CHANGED
@@ -20,7 +20,7 @@ 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
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';
@@ -411,10 +411,14 @@ 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
423
  // What the pool pays for ten units of its non-stable coin (coinA when neither or both are
420
424
  // stablecoins), quoted like a rule's price; ten clears DeepBook's minimum order.
@@ -438,10 +442,15 @@ export function createPluginMcp(registry) {
438
442
  }))),
439
443
  lending: Object.keys(d.protocols.navi.reserves).map((c) => coinInfo(d, c).symbol),
440
444
  ...(d.pools.length || Object.keys(d.protocols.navi.reserves).length
441
- ? {}
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
+ }
442
448
  : {
443
449
  note: 'Transfers and staking only: this network lists no pools and no NAVI reserves, so swap and lending rules are refused here.',
444
450
  }),
451
+ ...(owner
452
+ ? { ownerCoins: await readAllHoldings(clientFor(d), d, owner).catch(() => null) }
453
+ : {}),
445
454
  });
446
455
  const own = (await exists(path)) ? [await markets(await registry.deployment())] : [];
447
456
  const bundled = await Promise.all((await registry.bundledNetworks()).map(async (n) => markets(await registry.bundled(n))));
package/docs/PLAYBOOK.md CHANGED
@@ -53,13 +53,15 @@ it; say so when the owner picks one.
53
53
  pool), so list Cetus as well when trades will be small. A network may list no pools or no NAVI
54
54
  reserves; `list_passes` says so, and swap or lending rules are refused there.
55
55
 
56
- **Coins and pools.** Name a coin by symbol when `list_passes` lists it for the network. Name any
57
- other coin the owner holds (`session_info.ownerCoins` for an existing pass, or a `listBalances`
58
- read of the owner's address) by its full type, and give the id of a
59
- DeepBook or Cetus pool holding exactly that pair in `pools`. The server and the review page read
60
- such a coin's symbol and decimals and such a pool's venue from chain, never from you, and the
61
- page shows an unlisted coin with its type so the owner can tell a look-alike from the real one.
62
- Lending is limited to the coins the network lists as NAVI reserves.
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. Name any other coin
58
+ the owner holds by its full type, and for a swap give the id of a DeepBook or Cetus pool holding
59
+ exactly that pair in `pools`. To find the type and the balance, ask the owner for their address
60
+ and call `list_passes` with `owner`: it adds every coin that address holds per network. For the
61
+ pool, ask the owner or look the pair up on the venue's own site. The server and the review page
62
+ read such a coin's symbol and decimals and such a pool's venue from chain, never from you, and
63
+ the page shows an unlisted coin with its type so the owner can tell a look-alike from the real
64
+ one. Lending is limited to the coins the network lists as NAVI reserves.
63
65
 
64
66
  **Lifetime caps.** `caps` gives, per coin, the total the key may ever withdraw, for exactly the
65
67
  coins the rules spend (staking spends SUI). A daily limit may not exceed its coin's cap. Size
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/sui-agent-pass",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "AI builds transactions. A session signer signs. Your pass enforces the rules.",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "deployments",
27
27
  "docs/AGENT.md",
28
28
  "docs/PLAYBOOK.md",
29
+ "skills",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],
@@ -37,7 +38,7 @@
37
38
  "node": ">=22"
38
39
  },
39
40
  "scripts": {
40
- "build": "tsc && vite build",
41
+ "build": "tsc && vite build && node scripts/skill.mjs",
41
42
  "prepublishOnly": "npm run build",
42
43
  "check": "tsc -p tsconfig.tools.json && tsc -p tsconfig.web.json",
43
44
  "test": "node --import tsx --test tests/*.test.ts",
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: agent-pass
3
+ description: "Set up and use a Sui Agent Pass: turn a request such as \"sell up to 0.5 SUI a day for USDC at no less than 0.95, for a week\" into on-chain rules the owner signs once in their wallet, then trade, transfer, stake, lend and borrow within them through the agent-pass MCP server. Use for giving an agent money to manage on Sui, any Sui swap, transfer, staking or NAVI lending request, including coins not listed by default, checking or changing a pass, or ending one."
4
+ ---
5
+
6
+ # Agent playbook
7
+
8
+ How an agent sets up a pass from an owner's request, acts within its rules, reports on it,
9
+ changes it and ends it. Every MCP client gets this text as the `agent-pass://playbook` resource;
10
+ the SDK and the contract interface are in `agent-pass://guide` ([`docs/AGENT.md`](AGENT.md)).
11
+
12
+ ## How a pass works
13
+
14
+ The owner's coins stay in the owner's address. The owner signs once in their own wallet, and
15
+ that one transaction creates a pass holding the rules, issues allowances over the owner's
16
+ balance that only the pass can spend, and gives the agent key its gas. You act with that agent
17
+ key, which the signer holds and you never see. Each action is a transaction that draws on an
18
+ allowance and that the chain checks against the rules; proceeds go back to the owner's address,
19
+ never to you. A position you open (a NAVI account, a stake) sits in the pass, and only the owner
20
+ can take it out.
21
+
22
+ A signature from the signer is not permission. Only a successful on-chain execution is.
23
+
24
+ Respond in English. Amounts are exact decimals in each coin's own units; `list_passes` and
25
+ `session_info` give every coin's decimals. Dates are UTC, and a day is a UTC calendar day. Off
26
+ mainnet the stablecoin is a test token and the markets are stand-ins; transfers and staking are
27
+ real everywhere.
28
+
29
+ ## 1. Turn the request into rules
30
+
31
+ Map what the owner asks for onto the rule sets below. Anything they did not ask for is not
32
+ allowed: leave it out. One rule per coin, per swap direction and per lending coin; a coin without
33
+ a rule cannot be touched.
34
+
35
+ Three things only the owner can supply. Ask for them; never fill them in:
36
+
37
+ - **Recipients.** Every transfer goes to a named address the owner gave you.
38
+ - **Prices.** Every swap rule needs the owner's price limit. When they have not said one, show
39
+ them the pool's current price from `list_passes` and ask what they accept.
40
+ - **Amounts.** When a limit is missing, ask, or propose a conservative one and say that you did.
41
+
42
+ Every limit must be above zero; there is no "no limit".
43
+
44
+ | Rule set | Fields | What the chain enforces |
45
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | `transfers` | `coin`, named recipients (name and address), per transfer, per day | only those recipients ever receive that coin |
47
+ | `swaps` | `sell`, `buy`, optional `venues` (`deepbook`, `cetus`; every listed pool for the pair when omitted), per trade, per day, `limitPrice` | only listed pools; limits on the sold coin; every fill at the price limit or better |
48
+ | `lending` | `coin`, `allowBorrow`, per action, per day, `minHealthFactor` (or null) | NAVI only; supply and withdraw, plus borrow and repay when allowed; a withdraw or borrow that would take the health factor under the floor is refused |
49
+ | `staking` | validators (empty for any active one), SUI per stake, per day | only those validators; at least 1 SUI per stake; the stake stays in the pass |
50
+
51
+ **Prices.** When one coin of a pair is a stablecoin, `limitPrice` is in that stablecoin per one
52
+ of the other coin: the most the agent pays when buying, the least it accepts when selling. For
53
+ other pairs it is the bought coin per one sold coin, the least accepted. The limit is fixed when
54
+ the owner signs, so a limit close to the market refuses every trade once the price moves past
55
+ it; say so when the owner picks one.
56
+
57
+ **Venues.** DeepBook fills nothing under a pool's minimum order (1 SUI on the mainnet SUI/USDC
58
+ pool), so list Cetus as well when trades will be small. A network may list no pools or no NAVI
59
+ reserves; `list_passes` says so, and swap or lending rules are refused there.
60
+
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. Name any other coin
63
+ the owner holds by its full type, and for a swap give the id of a DeepBook or Cetus pool holding
64
+ exactly that pair in `pools`. To find the type and the balance, ask the owner for their address
65
+ and call `list_passes` with `owner`: it adds every coin that address holds per network. For the
66
+ pool, ask the owner or look the pair up on the venue's own site. The server and the review page
67
+ read such a coin's symbol and decimals and such a pool's venue from chain, never from you, and
68
+ the page shows an unlisted coin with its type so the owner can tell a look-alike from the real
69
+ one. Lending is limited to the coins the network lists as NAVI reserves.
70
+
71
+ **Lifetime caps.** `caps` gives, per coin, the total the key may ever withdraw, for exactly the
72
+ coins the rules spend (staking spends SUI). A daily limit may not exceed its coin's cap. Size
73
+ each cap to cover the days of the term, a little above the sum of the daily limits: the chain
74
+ checks the cap before the daily limit, so a cap equal to one day's limit turns a "daily limit"
75
+ refusal into a "lifetime cap" one.
76
+
77
+ **What counts against a limit.** Limits meter what leaves the owner's balance, not net exposure.
78
+ A supply or a stake counts in full, and withdrawing or unstaking returns the funds without giving
79
+ the allowance back. A supply-and-withdraw or stake-and-unstake cycle costs the allowance once, so
80
+ size lending and staking limits for the cycles the agent will run, and tell the owner when they
81
+ ask for something like "restake with a better validator".
82
+
83
+ **Term and gas.** `days` is the agent's term, 1 to 30. `gasSui` (default 0.05) is a stipend sent
84
+ to the agent key, outside the rules. `gasCapSui` (default 0.1) is a separate allowance the key
85
+ draws on to top up its gas, and `gasDailySui` (default a fifth of `gasCapSui`) its daily limit.
86
+
87
+ **What the owner needs.** Allowances draw only from the owner's address balance, never from coin
88
+ objects. The review page reads both, and the signature converts coin objects into the address
89
+ balance as far as each cap (plus `gasCapSui` for SUI) needs; the converted coins stay theirs. The
90
+ owner needs that much in total, address balance and coin objects together, plus `gasSui` and fees.
91
+
92
+ Before you call anything, show the owner the rules you intend to propose as one short list and
93
+ ask for a yes, unless they already stated every rule explicitly.
94
+
95
+ ## 2. Propose the pass and wait for the signature
96
+
97
+ `propose_authorization`, `propose_change`, `list_passes`, `revoke_pass` and `forget_pass` live on
98
+ the stdio server (`agent-pass mcp`), which keeps every session on the machine. A per-session HTTP
99
+ signer has only `session_info` and `sign_transaction`.
100
+
101
+ The pass goes on the network the owner names (`network`, one of `list_passes.networks`), else on
102
+ the first network listed. A private publish of one's own takes a `deployment` path instead.
103
+
104
+ Call `propose_authorization` with the rules. It returns `rules`, the exact text the review page
105
+ shows, plus `reviewUrl` and `sessionAddress`, and opens the page in the browser. Paste `rules` to
106
+ the owner word for word and never restate the amounts yourself. Tell them to connect their wallet,
107
+ check the page against that text and sign once. The page shows what their wallet holds against
108
+ what the pass needs. If the browser did not open, give them `reviewUrl`.
109
+
110
+ Then call `session_info` with the pass and `wait: 120`, again while the status is
111
+ `awaiting_authorization`. At `active` the signer has read the pass, the allowances and the
112
+ policies back from chain and checked them against the proposal. Report the pass id, the
113
+ authorization digest, the owner's balances and what the key may still withdraw.
114
+
115
+ ## 3. Act within the rules
116
+
117
+ Start with `session_info`. It returns:
118
+
119
+ - `control`: the installed policies with today's usage, the held stakes and NAVI position;
120
+ - `allowances`: what the key may still withdraw under each;
121
+ - `ownerBalances`: per coin, `addressBalance` (what allowances draw from) and `coinBalance` (coin
122
+ objects only the owner can move), so a low address balance next to coin objects is not an owner
123
+ out of money;
124
+ - `gas`: the agent key's own SUI;
125
+ - `coins` and `venues`: each coin's type and decimals, and each pool with its pair, both
126
+ directions' entry functions and DeepBook's minimum order;
127
+ - `directory`, `sdk` and `server`: the session directory, how to install and import the SDK,
128
+ and this server's version (after an upgrade the client must restart the server).
129
+
130
+ Act by writing a Node script against the SDK. Run `sdk.install` once in your working directory,
131
+ then:
132
+
133
+ ```ts
134
+ import {
135
+ Agent,
136
+ borrow,
137
+ pickValidator,
138
+ quote,
139
+ repay,
140
+ send,
141
+ stake,
142
+ supply,
143
+ swap,
144
+ unstake,
145
+ withdraw,
146
+ } from '@mysten-incubation/sui-agent-pass/agent';
147
+ import { units } from '@mysten-incubation/sui-agent-pass/domain';
148
+
149
+ const agent = await Agent.open(directory);
150
+ const usdc = agent.identity.deployment.coins.find((c) => c.symbol === 'USDC')!;
151
+ const out = await agent.submit(send(agent.identity, usdc.type, units('5', usdc.decimals), 'alice'));
152
+ ```
153
+
154
+ **Swaps.** Before every swap, `quote(identity, pair, amountIn)` prices it on the venue without
155
+ touching an allowance: `amountIn` is what the pool would take and `amountOut` what comes back.
156
+ Set `minOut` a little under `amountOut`, never from a mid price; the owner's price limit still
157
+ applies underneath. DeepBook takes its fee from the input and rounds down to its lot size, so
158
+ sell at least one lot over its minimum.
159
+
160
+ **Repayments.** Size a repayment from the live debt: an over-payment returns to the owner but
161
+ still counts against the allowance.
162
+
163
+ **Outcomes.** `submit` signs through this server, executes on Sui and returns what the chain
164
+ recorded. `success: true` means done. `success: false` means the chain executed the transaction
165
+ and refused it; `reason` is the owner's rule that stopped it, and you quote it. A thrown error
166
+ means nothing executed (a dry run or client error), which you never describe as on-chain
167
+ enforcement. `submit` tops up your gas from the gas allowance when it runs low.
168
+
169
+ **Hand-built transactions.** For anything the builders do not cover, build the PTB with
170
+ `@mysten/sui` following the guide, sign it with `sign_transaction` and execute it yourself. Every
171
+ spend takes the allowance object and a withdrawal input right after the pass. On this path gas is
172
+ yours: when the key holds under 0.01 SUI, draw on the gas allowance as the guide shows. That
173
+ allowance has its own cap and daily limit and never touches the asset caps.
174
+
175
+ ## 4. Report the state
176
+
177
+ Call `list_passes`, then `session_info` for each pass the owner asks about (all of them when
178
+ unclear). Report the status (`awaiting_authorization`, `active`, `changing`, `expired`, `revoked`
179
+ or `closed`), the owner's address balances, what the key may still withdraw, its gas, any NAVI
180
+ position or stakes in the pass, and for each policy its limit and today's usage
181
+ (`control.policies[*].usage.usedToday`). Quote the `rules` text as returned; do not recompute
182
+ amounts.
183
+
184
+ ## 5. Change the rules
185
+
186
+ When a limit, a recipient, a pool, a price or the term no longer fits, change the pass instead of
187
+ proposing a new one. Call `propose_change` with the pass and the complete new rule set (every
188
+ rule, not just the changed ones), in the same shape as `propose_authorization` without the name
189
+ and the stipend. It opens the review page with the current rules beside the new ones; paste its
190
+ `changes` text to the owner. One signature replaces the policies and reissues only the
191
+ allowances whose cap or term changed. The agent key, its gas and what the pass holds stay;
192
+ replaced policies start the day's usage at zero, and kept allowances keep their lifetime spend.
193
+
194
+ Until the owner signs, the old rules apply and `session_info` reports `changing`. Poll it with
195
+ `wait` until it is `active` again, then read the rules back before acting.
196
+
197
+ ## 6. End it
198
+
199
+ There are two endings, both on the pass page, which `revoke_pass` opens for the pass the owner
200
+ names. Without a pass it opens the owner page, which lists all of the owner's passes and ends any
201
+ number of them with one signature; use that when several are to go.
202
+
203
+ - **Stop the agent now** revokes the key and deletes the allowances, and keeps the pass and its
204
+ history on chain. It is the instant answer when something is wrong.
205
+ - **Close the pass** does the same, then returns everything the pass holds to the owner's wallet
206
+ and deletes the pass, which refunds its storage deposit. A NAVI position that owes nothing is
207
+ withdrawn into the owner's wallet first, since NAVI's own app cannot operate a pass's position.
208
+ A position with debt is handed over as it is, and the owner page lists it with its balances.
209
+ This is the normal ending when the owner is done.
210
+
211
+ Tell the owner both exist and that closing refunds the storage cost. They connect the wallet that
212
+ created the pass, pick an ending and sign. Opening the page revokes nothing; only the signed
213
+ transaction does. Then call `session_info` with `wait: 60` until the status is `revoked` or
214
+ `closed`, and confirm that the chain now refuses the key. On a stopped pass, each held object has
215
+ its own **Reclaim** button: a NAVI position comes back with its account cap (collateral first
216
+ when it owes nothing), and a stake as an ordinary `StakedSui`.
217
+
218
+ Once a pass is closed, the signer returns the key's unspent gas to the owner the next time it
219
+ runs. Then call `forget_pass` for the session: it refuses while the pass is live, returns any gas
220
+ still there, and deletes the session directory with its key. Offer it for every pass
221
+ `list_passes` reports as closed or stopped, and for proposals the owner never signed.
222
+
223
+ `list_passes` may also report `unreadable` sessions from an earlier version of this package.
224
+ Their passes may still be live: tell the owner to stop or close each one from its pass page on
225
+ the site, which reads the chain and needs no session file.
226
+
227
+ ## Rules of conduct
228
+
229
+ - Confirm with the owner before the first transaction of a session and before any action they
230
+ did not explicitly ask for. Recurring work they asked for, such as a daily purchase, needs no
231
+ confirmation each time.
232
+ - Save the digest of every submission. If a response times out, look the digest up before
233
+ retrying; never resubmit an action while the original could still execute.
234
+ - Never search for, read or copy key files. `sign_transaction` is the only way to sign.
235
+ - Report outcomes from chain state (`session_info`, the digest), never from a signature.