@floatonnear/mcp 0.10.1 → 0.10.3

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.
@@ -43,9 +43,11 @@ export interface OwnerPayload {
43
43
  /** Non-empty only when the action is destructive or hard to undo. */
44
44
  warnings: string[];
45
45
  }
46
- export declare function planAddNode(vault: string, parent: number | null, label: string, limits: HumanTokenLimit[], cooldownSec: number, recipients: string[] | null): OwnerPayload;
47
- export declare function planSetNodeCaps(vault: string, nodeId: number, limits: HumanTokenLimit[]): OwnerPayload;
48
- export declare function planBindAgent(vault: string, nodeId: number, accountId: string): OwnerPayload;
46
+ export declare function planAddNode(vault: string, parent: number | null, label: string, limits: HumanTokenLimit[], cooldownSec: number, recipients: string[] | null,
47
+ /** The PARENT's human label, so the summary can say "under news". */
48
+ parentName?: string | null): OwnerPayload;
49
+ export declare function planSetNodeCaps(vault: string, nodeId: number, limits: HumanTokenLimit[], nodeName?: string | null): OwnerPayload;
50
+ export declare function planBindAgent(vault: string, nodeId: number, accountId: string, nodeName?: string | null): OwnerPayload;
49
51
  /**
50
52
  * A whole departmental agent in ONE owner signature: node, binding, gas key.
51
53
  *
@@ -69,7 +71,7 @@ export declare function planBindAgent(vault: string, nodeId: number, accountId:
69
71
  * and the vault carries the owner's full-access key.
70
72
  */
71
73
  export declare function planAddRole(vault: string, label: string, limits: HumanTokenLimit[], cooldownSec: number, recipients: string[] | null, publicKey: string, fundingYocto: string): OwnerPayload;
72
- export declare function planBindVirtualAgent(vault: string, nodeId: number, publicKey: string): OwnerPayload;
74
+ export declare function planBindVirtualAgent(vault: string, nodeId: number, publicKey: string, nodeName?: string | null): OwnerPayload;
73
75
  /**
74
76
  * Revoke a virtual agent's authority.
75
77
  *
@@ -80,9 +82,9 @@ export declare function planBindVirtualAgent(vault: string, nodeId: number, publ
80
82
  * half the binding's storage is intentionally unrecoverable.
81
83
  */
82
84
  export declare function planUnbindVirtualAgent(vault: string, publicKey: string): OwnerPayload;
83
- export declare function planSetRecipients(vault: string, nodeId: number, recipients: string[] | null): OwnerPayload;
84
- export declare function planSetCooldown(vault: string, nodeId: number, cooldownSec: number): OwnerPayload;
85
- export declare function planFreezeNode(vault: string, nodeId: number, frozen: boolean): OwnerPayload;
85
+ export declare function planSetRecipients(vault: string, nodeId: number, recipients: string[] | null, nodeName?: string | null): OwnerPayload;
86
+ export declare function planSetCooldown(vault: string, nodeId: number, cooldownSec: number, nodeName?: string | null): OwnerPayload;
87
+ export declare function planFreezeNode(vault: string, nodeId: number, frozen: boolean, nodeName?: string | null): OwnerPayload;
86
88
  export declare function planSetPaused(vault: string, paused: boolean): OwnerPayload;
87
89
  export declare function planSetStrictAllocation(vault: string, strict: boolean): OwnerPayload;
88
90
  export declare function planPurgeSettled(vault: string, beforeDay: number, limit: number): OwnerPayload;
@@ -195,7 +197,7 @@ export declare function toCliCommand(p: OwnerPayload, network?: string): string;
195
197
  * `toBaseUnits` throws rather than truncating excess precision — correct here:
196
198
  * silently rounding 0.1234567 would set a threshold the owner did not choose.
197
199
  */
198
- export declare function planSetApprovalThreshold(vault: string, nodeId: number, amount: string | null, decimals: number): OwnerPayload;
200
+ export declare function planSetApprovalThreshold(vault: string, nodeId: number, amount: string | null, decimals: number, nodeName?: string | null): OwnerPayload;
199
201
  /**
200
202
  * Execute a held payment.
201
203
  *
package/dist/payloads.js CHANGED
@@ -105,7 +105,9 @@ function assertCooldown(cooldownSec) {
105
105
  throw new Error(`cooldown must be at most ${MAX_COOLDOWN_SEC}s (MAX_COOLDOWN_SEC)`);
106
106
  }
107
107
  }
108
- export function planAddNode(vault, parent, label, limits, cooldownSec, recipients) {
108
+ export function planAddNode(vault, parent, label, limits, cooldownSec, recipients,
109
+ /** The PARENT's human label, so the summary can say "under news". */
110
+ parentName) {
109
111
  if (!label || label.length > MAX_LABEL_LEN) {
110
112
  throw new Error(`label must be 1..=${MAX_LABEL_LEN} characters (MAX_LABEL_LEN)`);
111
113
  }
@@ -123,35 +125,99 @@ export function planAddNode(vault, parent, label, limits, cooldownSec, recipient
123
125
  },
124
126
  gasTgas: DEFAULT_GAS_TGAS,
125
127
  depositYocto: ONE_YOCTO,
126
- summary: `Create budget node "${label}" ${parent === null ? "as the ROOT" : `under node ${parent}`}` +
128
+ summary:
129
+ // "the main budget" for a root, and the PARENT's name otherwise — a new
130
+ // child is described by where it hangs, which is the one thing the
131
+ // person choosing it cares about.
132
+ `Create a "${label}" budget ` +
133
+ `${parent === null ? "as the main budget" : `under ${budgetName(parent, parentName)}`}` +
127
134
  ` with ${describeLimits(limits)}` +
128
135
  (cooldownSec ? `, ${cooldownSec}s cooldown between payments` : "") +
129
136
  (recipients ? `, paying only: ${recipients.join(", ")}` : `, inheriting the payee allowlist`),
130
137
  warnings: [],
131
138
  };
132
139
  }
133
- export function planSetNodeCaps(vault, nodeId, limits) {
140
+ /**
141
+ * How a budget is NAMED to a person.
142
+ *
143
+ * WHY THIS EXISTS. Every plan summary said "node 1", and `planVoice` copies
144
+ * the summary verbatim into the `say` a user reads — so the product spoke the
145
+ * tree's internal vocabulary: "Set node 1 caps to 1/day", "Freeze node 0".
146
+ * A node id is the contract's primary key. It is exactly right in `args`, in
147
+ * `technical`, and in the unsigned payload a wallet signs, and it is a
148
+ * stranger's filing reference to the person who called the thing "news".
149
+ *
150
+ * The label is NOT derivable here — it lives on chain, and this module is pure
151
+ * and synchronously testable, which is worth keeping. So the caller resolves
152
+ * it and passes it in; `server.ts` already fetches the tree to check the node
153
+ * exists, so this costs no extra call.
154
+ *
155
+ * FALLBACKS, in order of how much they can honestly claim:
156
+ * a label -> `the "news" budget`
157
+ * node 0, no label -> "the main budget" (root is the only id with a meaning)
158
+ * anything else -> "this budget" (true, and says nothing false)
159
+ *
160
+ * "this budget" is deliberately vague rather than wrong. The alternative was
161
+ * falling back to the id, which is the bug.
162
+ */
163
+ /**
164
+ * Sentence-case a budget name for the start of a summary.
165
+ *
166
+ * `budgetName` returns a lowercase noun phrase because most of its uses sit
167
+ * mid-sentence ("Freeze the main budget"). A few lead the sentence, and
168
+ * "the main budget must wait 30s" reads like a fragment.
169
+ */
170
+ function capitalise(s) {
171
+ return s.charAt(0).toUpperCase() + s.slice(1);
172
+ }
173
+ function budgetName(nodeId, label) {
174
+ /**
175
+ * ROOT IS ALWAYS "the main budget", whatever its stored label.
176
+ *
177
+ * The contract hardcodes root's label to the literal string "root"
178
+ * (`lib.rs`: `label: "root".to_string()`), which is the tree's word, not a
179
+ * person's. `the "root" budget` would just be "node 0" wearing a hat.
180
+ */
181
+ if (nodeId === 0)
182
+ return "the main budget";
183
+ const clean = typeof label === "string" ? label.trim() : "";
184
+ if (clean)
185
+ return `the "${clean}" budget`;
186
+ /**
187
+ * NO LABEL means the tree read FAILED — `Node.label` is a required `String`
188
+ * on chain (`types.rs`), so every real node has one. "this budget" is
189
+ * deliberately vague rather than wrong; the alternative was falling back to
190
+ * the id, which is the thing being removed.
191
+ *
192
+ * It does cost distinguishability in that degraded case: two different
193
+ * budgets read the same. The node id is still in `args.node_id`, which is
194
+ * what the WALLET displays and what the owner actually signs, so the
195
+ * signature is never ambiguous even when the sentence is. Pinned by a test.
196
+ */
197
+ return "this budget";
198
+ }
199
+ export function planSetNodeCaps(vault, nodeId, limits, nodeName) {
134
200
  return {
135
201
  receiverId: vault,
136
202
  methodName: "set_node_caps",
137
203
  args: { node_id: nodeId, limits: limitsToArgs(limits) },
138
204
  gasTgas: DEFAULT_GAS_TGAS,
139
205
  depositYocto: ONE_YOCTO,
140
- summary: `Set node ${nodeId} caps to ${describeLimits(limits)}`,
206
+ summary: `Update ${budgetName(nodeId, nodeName)} to ${describeLimits(limits)}`,
141
207
  warnings: [],
142
208
  };
143
209
  }
144
- export function planBindAgent(vault, nodeId, accountId) {
210
+ export function planBindAgent(vault, nodeId, accountId, nodeName) {
145
211
  return {
146
212
  receiverId: vault,
147
213
  methodName: "bind_agent",
148
214
  args: { node_id: nodeId, account_id: accountId },
149
215
  gasTgas: DEFAULT_GAS_TGAS,
150
216
  depositYocto: ONE_YOCTO,
151
- summary: `Let ${accountId} spend from node ${nodeId}`,
217
+ summary: `Let ${accountId} spend from ${budgetName(nodeId, nodeName)}`,
152
218
  warnings: [
153
219
  `${accountId} is an EXISTING account whose keys you do not control. ` +
154
- `It can call request_payment with the full authority of node ${nodeId}.`,
220
+ `It can spend with the full authority of ${budgetName(nodeId, nodeName)}.`,
155
221
  `Prefer a VIRTUAL agent (bind_virtual_agent): it binds one key you generate ` +
156
222
  `rather than a whole account, costs ~168 bytes, and strands nothing.`,
157
223
  ],
@@ -218,7 +284,7 @@ export function planAddRole(vault, label, limits, cooldownSec, recipients, publi
218
284
  ],
219
285
  };
220
286
  }
221
- export function planBindVirtualAgent(vault, nodeId, publicKey) {
287
+ export function planBindVirtualAgent(vault, nodeId, publicKey, nodeName) {
222
288
  assertNodeId(nodeId);
223
289
  assertPublicKey(publicKey);
224
290
  return {
@@ -230,7 +296,7 @@ export function planBindVirtualAgent(vault, nodeId, publicKey) {
230
296
  // `assert_owner()` -> `assert_one_yocto()`. Nothing is funded here, which
231
297
  // is the whole point of moving off sub-accounts.
232
298
  depositYocto: ONE_YOCTO,
233
- summary: `Let the key ${publicKey.slice(0, 22)}… spend from node ${nodeId}`,
299
+ summary: `Let a new agent key spend from ${budgetName(nodeId, nodeName)}`,
234
300
  warnings: [
235
301
  `A node may hold more than one key — binding another does not replace this one.`,
236
302
  `This grants spending authority only. The key still needs gas: install it as ` +
@@ -265,7 +331,7 @@ export function planUnbindVirtualAgent(vault, publicKey) {
265
331
  ],
266
332
  };
267
333
  }
268
- export function planSetRecipients(vault, nodeId, recipients) {
334
+ export function planSetRecipients(vault, nodeId, recipients, nodeName) {
269
335
  assertRecipients(recipients);
270
336
  return {
271
337
  receiverId: vault,
@@ -274,17 +340,18 @@ export function planSetRecipients(vault, nodeId, recipients) {
274
340
  gasTgas: DEFAULT_GAS_TGAS,
275
341
  depositYocto: ONE_YOCTO,
276
342
  summary: recipients
277
- ? `Node ${nodeId} may pay only: ${recipients.join(", ")}`
278
- : `Node ${nodeId} inherits its payee allowlist from the nearest ancestor that sets one`,
343
+ ? `${capitalise(budgetName(nodeId, nodeName))} may pay only: ${recipients.join(", ")}`
344
+ : `${capitalise(budgetName(nodeId, nodeName))} takes its list of allowed payees from the budget above it`,
279
345
  warnings: recipients
280
346
  ? [
281
- `This REPLACES the inherited allowlist for node ${nodeId} and its descendants; ` +
282
- `it does not add to it. Payees it currently inherits will stop being payable.`,
347
+ `This REPLACES the inherited payee list for ${budgetName(nodeId, nodeName)} and ` +
348
+ `everything under it; it does not add to it. Payees it currently inherits will ` +
349
+ `stop being payable.`,
283
350
  ]
284
351
  : [],
285
352
  };
286
353
  }
287
- export function planSetCooldown(vault, nodeId, cooldownSec) {
354
+ export function planSetCooldown(vault, nodeId, cooldownSec, nodeName) {
288
355
  assertCooldown(cooldownSec);
289
356
  return {
290
357
  receiverId: vault,
@@ -293,21 +360,23 @@ export function planSetCooldown(vault, nodeId, cooldownSec) {
293
360
  gasTgas: DEFAULT_GAS_TGAS,
294
361
  depositYocto: ONE_YOCTO,
295
362
  summary: cooldownSec
296
- ? `Node ${nodeId} must wait ${cooldownSec}s between payments`
297
- : `Remove the cooldown on node ${nodeId}`,
363
+ ? `${capitalise(budgetName(nodeId, nodeName))} must wait ${cooldownSec}s between payments`
364
+ : `Remove the waiting time between payments on ${budgetName(nodeId, nodeName)}`,
298
365
  warnings: [],
299
366
  };
300
367
  }
301
- export function planFreezeNode(vault, nodeId, frozen) {
368
+ export function planFreezeNode(vault, nodeId, frozen, nodeName) {
302
369
  return {
303
370
  receiverId: vault,
304
371
  methodName: "freeze_node",
305
372
  args: { node_id: nodeId, frozen },
306
373
  gasTgas: DEFAULT_GAS_TGAS,
307
374
  depositYocto: ONE_YOCTO,
308
- summary: frozen ? `Freeze node ${nodeId}` : `Unfreeze node ${nodeId}`,
375
+ summary: frozen
376
+ ? `Freeze ${budgetName(nodeId, nodeName)} so nothing under it can pay until you unfreeze it`
377
+ : `Unfreeze ${budgetName(nodeId, nodeName)} so it can pay again`,
309
378
  warnings: frozen
310
- ? [`This blocks payments from node ${nodeId} AND every node in its subtree.`]
379
+ ? [`This stops payments from ${budgetName(nodeId, nodeName)} and every budget under it.`]
311
380
  : [],
312
381
  };
313
382
  }
@@ -466,7 +535,7 @@ export function toCliCommand(p, network = "testnet") {
466
535
  * `toBaseUnits` throws rather than truncating excess precision — correct here:
467
536
  * silently rounding 0.1234567 would set a threshold the owner did not choose.
468
537
  */
469
- export function planSetApprovalThreshold(vault, nodeId, amount, decimals) {
538
+ export function planSetApprovalThreshold(vault, nodeId, amount, decimals, nodeName) {
470
539
  const base = amount === null ? null : toBaseUnits(amount, decimals).toString();
471
540
  return {
472
541
  receiverId: vault,
@@ -478,8 +547,8 @@ export function planSetApprovalThreshold(vault, nodeId, amount, decimals) {
478
547
  // 1000000 (base units)", which is unreadable in precisely the moment the
479
548
  // owner is checking the plan before signing it.
480
549
  summary: amount
481
- ? `Payments above ${amount} from node ${nodeId} will need your approval`
482
- : `Remove the approval threshold on node ${nodeId} — payments up to the caps run unattended`,
550
+ ? `Payments above ${amount} from ${budgetName(nodeId, nodeName)} will need your approval`
551
+ : `Stop requiring your approval on ${budgetName(nodeId, nodeName)} — payments inside the limits run unattended`,
483
552
  warnings: amount
484
553
  ? []
485
554
  : ["After this, nothing at this node waits for a human: only the caps apply."],
package/dist/roles.d.ts CHANGED
@@ -4,6 +4,26 @@ export interface RoleCredential {
4
4
  role: string;
5
5
  nodeId?: number;
6
6
  }
7
+ /**
8
+ * Entries belonging to this vault that the loader REJECTS, and why.
9
+ *
10
+ * `loadable` drops anything without a string `secret`, and the reasoning is
11
+ * sound — the adapter applies the same predicate, so listing such an entry
12
+ * would advertise an agent that can never be selected. But DROPPING IT
13
+ * SILENTLY is the same failure this file already has a memory about: the user
14
+ * holds a key, the tool shows nothing, and there is no way to tell "no agent"
15
+ * from "an agent in a shape I refuse to read".
16
+ *
17
+ * Shapes do vary in the wild: `prove-refusal.mjs` reads
18
+ * `privateKey ?? secret ?? agentKey`, so a file written by another path can
19
+ * land here unreadable. Reported, not hidden.
20
+ */
21
+ export declare function listUnloadable(cfg: {
22
+ agentKeysPath?: string;
23
+ vault?: string;
24
+ }): {
25
+ reason: string;
26
+ }[];
7
27
  export declare class RoleSelectionError extends Error {
8
28
  }
9
29
  export declare function selectRole(cfg: {
@@ -43,4 +63,13 @@ export declare function listAgents(cfg: {
43
63
  agentId: string;
44
64
  role: string | null;
45
65
  nodeId?: number;
66
+ /**
67
+ * THE PUBLIC half, so a caller can ask the CHAIN about this key.
68
+ *
69
+ * It was omitted, and that omission is what made `vault_roles` unable to
70
+ * tell a working agent from a dead one: with no public key there is nothing
71
+ * to look up, so the only thing it could report was what the local file
72
+ * claimed. No key material — the public key is public.
73
+ */
74
+ publicKey?: string;
46
75
  }[];
package/dist/roles.js CHANGED
@@ -11,6 +11,54 @@
11
11
  * as a credential is how an agent ends up "configured" and unreachable.
12
12
  */
13
13
  import { existsSync, readFileSync } from "node:fs";
14
+ /**
15
+ * Entries belonging to this vault that the loader REJECTS, and why.
16
+ *
17
+ * `loadable` drops anything without a string `secret`, and the reasoning is
18
+ * sound — the adapter applies the same predicate, so listing such an entry
19
+ * would advertise an agent that can never be selected. But DROPPING IT
20
+ * SILENTLY is the same failure this file already has a memory about: the user
21
+ * holds a key, the tool shows nothing, and there is no way to tell "no agent"
22
+ * from "an agent in a shape I refuse to read".
23
+ *
24
+ * Shapes do vary in the wild: `prove-refusal.mjs` reads
25
+ * `privateKey ?? secret ?? agentKey`, so a file written by another path can
26
+ * land here unreadable. Reported, not hidden.
27
+ */
28
+ export function listUnloadable(cfg) {
29
+ const path = cfg.agentKeysPath ?? ".float/agent-keys.json";
30
+ if (!existsSync(path))
31
+ return [];
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(readFileSync(path, "utf8"));
35
+ }
36
+ catch (e) {
37
+ return [{ reason: `${path} is not readable JSON (${e.message}). A UTF-8 BOM does this.` }];
38
+ }
39
+ if (!Array.isArray(parsed))
40
+ return [{ reason: `${path} is not a JSON array.` }];
41
+ const out = [];
42
+ for (const e of parsed) {
43
+ if (!e || typeof e !== "object")
44
+ continue;
45
+ const id = (e.agentId ?? e.vault);
46
+ // Only this vault's entries; another vault's are not this tool's business.
47
+ if (cfg.vault && id !== cfg.vault)
48
+ continue;
49
+ if (typeof e.agentId === "string" && typeof e.secret === "string")
50
+ continue; // loadable
51
+ const has = ["secret", "privateKey", "agentKey"].filter((k) => typeof e[k] === "string");
52
+ out.push({
53
+ reason: typeof e.agentId !== "string"
54
+ ? `an entry for this vault has no string 'agentId', so it cannot be matched to a vault.`
55
+ : `an entry has no 'secret' field${has.length ? ` (it uses '${has.join("', '")}' instead)` : ""}, ` +
56
+ `which is the only field the keyring loader reads. It will never be selectable. ` +
57
+ `Re-create the agent with vault_add_role.`,
58
+ });
59
+ }
60
+ return out;
61
+ }
14
62
  /** Entries that `loadKeyring` would accept, nothing else. */
15
63
  function loadable(path) {
16
64
  if (!existsSync(path))
@@ -113,5 +161,6 @@ export function listAgents(cfg) {
113
161
  agentId: e.agentId,
114
162
  role: typeof e.role === "string" ? e.role : null,
115
163
  nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
164
+ publicKey: typeof e.publicKey === "string" ? e.publicKey : undefined,
116
165
  }));
117
166
  }