@floatonnear/mcp 0.4.0 → 0.5.0

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.
@@ -1,10 +1,22 @@
1
1
  /**
2
2
  * The node a creation-time agent is bound to.
3
3
  *
4
- * ROOT_NODE in the vault. The factory binds at the root because it is the only
5
- * node that exists at `new` — children are added later by the owner.
4
+ * ONE, NOT ZERO, SINCE 2026-11-08. `new` used to bind at ROOT_NODE because it
5
+ * was the only node that existed — which left the FIRST agent holding the
6
+ * whole vault budget while every agent added later through `add_role` got a
7
+ * child of its own. The hierarchy only began constraining from agent #2.
8
+ *
9
+ * `new` now creates the agent its own child of root, carrying the same limits,
10
+ * exactly as `add_role` does. Root is 0 and that child is the first node
11
+ * inserted after it, so the agent is 1.
12
+ *
13
+ * PREDICTED, NOT READ, and that is unavoidable: the credential is generated
14
+ * before the vault exists, so there is nothing to ask. It is pinned by
15
+ * `binding_at_init_matches_binding_afterwards` in the contract, which asserts
16
+ * the node is 1 and not 0 — if `new` ever inserts another node first, that
17
+ * test fails rather than this constant drifting in silence.
6
18
  */
7
- export declare const AGENT_NODE_ID = 0;
19
+ export declare const AGENT_NODE_ID = 1;
8
20
  export interface GeneratedAgentKey {
9
21
  publicKey: string;
10
22
  /** The role this credential was filed under, when one was given. */
package/dist/agent-key.js CHANGED
@@ -21,10 +21,22 @@ import { KeyPair } from "near-api-js";
21
21
  /**
22
22
  * The node a creation-time agent is bound to.
23
23
  *
24
- * ROOT_NODE in the vault. The factory binds at the root because it is the only
25
- * node that exists at `new` — children are added later by the owner.
24
+ * ONE, NOT ZERO, SINCE 2026-11-08. `new` used to bind at ROOT_NODE because it
25
+ * was the only node that existed — which left the FIRST agent holding the
26
+ * whole vault budget while every agent added later through `add_role` got a
27
+ * child of its own. The hierarchy only began constraining from agent #2.
28
+ *
29
+ * `new` now creates the agent its own child of root, carrying the same limits,
30
+ * exactly as `add_role` does. Root is 0 and that child is the first node
31
+ * inserted after it, so the agent is 1.
32
+ *
33
+ * PREDICTED, NOT READ, and that is unavoidable: the credential is generated
34
+ * before the vault exists, so there is nothing to ask. It is pinned by
35
+ * `binding_at_init_matches_binding_afterwards` in the contract, which asserts
36
+ * the node is 1 and not 0 — if `new` ever inserts another node first, that
37
+ * test fails rather than this constant drifting in silence.
26
38
  */
27
- export const AGENT_NODE_ID = 0;
39
+ export const AGENT_NODE_ID = 1;
28
40
  /**
29
41
  * Write the agent's credential where the adapter will actually find it.
30
42
  *
package/dist/create.d.ts CHANGED
@@ -24,13 +24,18 @@
24
24
  * caught the drift the moment the contract shrank, which is the entire
25
25
  * reason it exists.
26
26
  *
27
+ * 298,819 -> 298,920 on 2026-11-08, when `new` began giving a creation-time
28
+ * agent its OWN child node instead of binding it to root. +101 bytes, about
29
+ * 0.00101 NEAR more per vault. The pin caught this one too, on the first gate
30
+ * run after the rebuild.
31
+ *
27
32
  * Mirrors `packages/create-float/res/vault.wasm`, and `create.pins.test.ts`
28
33
  * reads that file and fails if the two disagree — so a contract rebuild cannot
29
34
  * silently leave this quoting a deposit that is too small. Too small is not a
30
35
  * clean failure: CreateAccount succeeds and the vault sits below its own
31
36
  * storage requirement, bricking on the first write.
32
37
  */
33
- export declare const CONTRACT_BYTES = 298819;
38
+ export declare const CONTRACT_BYTES = 298920;
34
39
  /** NEAR charges 1e19 yoctoNEAR per byte of storage. */
35
40
  export declare const STORAGE_COST_PER_BYTE: bigint;
36
41
  /**
@@ -131,6 +136,26 @@ export interface CreatePlanInput {
131
136
  appUrl: string;
132
137
  /** Whether the derived account is already on chain. */
133
138
  exists?: boolean;
139
+ /**
140
+ * The EXISTING account's own `get_config.owner`, when it could be read.
141
+ *
142
+ * WHY THIS IS NOT OPTIONAL REASONING. "The account at your derived id exists,
143
+ * therefore it is yours" is an inference from the derivation, not a fact
144
+ * about the account — and `derive_prefix` IS NOT INJECTIVE. The factory says
145
+ * so in capitals, and `derive_prefix_collides_between_a_dot_and_a_hyphen`
146
+ * asserts it: `a.b.testnet` and `a-b.testnet` both land on `a-b-testnet`,
147
+ * because replacing the dot merges it into an alphabet that already contains
148
+ * the hyphen. Whoever creates first takes the id.
149
+ *
150
+ * Without reading the vault back, this tool would tell the LOSER of that
151
+ * collision to point their agent at the WINNER's vault. Narrow, but the
152
+ * failure is "you are now configured against someone else's money".
153
+ *
154
+ * `null` means the read failed, or the account is not a Float vault at all.
155
+ * That is a THIRD outcome and must not be flattened into either of the
156
+ * others — "I could not check" is not "it is yours".
157
+ */
158
+ existingOwner?: string | null;
134
159
  /**
135
160
  * The agent public key to bind at creation, if any.
136
161
  *
package/dist/create.js CHANGED
@@ -24,13 +24,18 @@
24
24
  * caught the drift the moment the contract shrank, which is the entire
25
25
  * reason it exists.
26
26
  *
27
+ * 298,819 -> 298,920 on 2026-11-08, when `new` began giving a creation-time
28
+ * agent its OWN child node instead of binding it to root. +101 bytes, about
29
+ * 0.00101 NEAR more per vault. The pin caught this one too, on the first gate
30
+ * run after the rebuild.
31
+ *
27
32
  * Mirrors `packages/create-float/res/vault.wasm`, and `create.pins.test.ts`
28
33
  * reads that file and fails if the two disagree — so a contract rebuild cannot
29
34
  * silently leave this quoting a deposit that is too small. Too small is not a
30
35
  * clean failure: CreateAccount succeeds and the vault sits below its own
31
36
  * storage requirement, bricking on the first write.
32
37
  */
33
- export const CONTRACT_BYTES = 298819;
38
+ export const CONTRACT_BYTES = 298920;
34
39
  /** NEAR charges 1e19 yoctoNEAR per byte of storage. */
35
40
  export const STORAGE_COST_PER_BYTE = 10n ** 19n;
36
41
  /**
@@ -139,8 +144,25 @@ export function buildCreatePlan(i) {
139
144
  `64-character limit. Use a named account.`);
140
145
  }
141
146
  if (known && i.exists) {
142
- throw new Error(`${i.vaultId} already exists. That is this owner's vault — connect to it ` +
143
- `with NEAR_VAULT_CONTRACT=${i.vaultId} rather than creating another.`);
147
+ // Three outcomes, deliberately not two. The old message asserted the
148
+ // first one for all three.
149
+ if (i.existingOwner && i.existingOwner === i.owner) {
150
+ throw new Error(`${i.vaultId} already exists and ${i.owner} owns it — confirmed by reading ` +
151
+ `its get_config. Connect to it with NEAR_VAULT_CONTRACT=${i.vaultId} ` +
152
+ `rather than creating another.`);
153
+ }
154
+ if (i.existingOwner) {
155
+ throw new Error(`${i.vaultId} already exists and is owned by ${i.existingOwner}, NOT by ` +
156
+ `${i.owner}. Do NOT point an agent at it.\n\n` +
157
+ `The factory derives a vault id by replacing dots with hyphens, and that ` +
158
+ `is not injective: "a.b.near" and "a-b.near" both derive "a-b-near", and ` +
159
+ `whoever created first holds the id. Create yours under a label instead, ` +
160
+ `which puts it at a different address.`);
161
+ }
162
+ throw new Error(`${i.vaultId} already exists, but its owner could NOT be read, so whether it ` +
163
+ `is yours is unknown — not assumed. It may not be a Float vault at all. ` +
164
+ `Check it before pointing anything at it:\n` +
165
+ ` near view ${i.vaultId} get_config`);
144
166
  }
145
167
  // Two different floors, and the quote must clear BOTH.
146
168
  //
package/dist/server.js CHANGED
@@ -183,7 +183,7 @@ const limitShape = z
183
183
  // startup: a wrong relative path would break the server outright instead of
184
184
  // merely reporting a stale version. server.smoke.test.ts asserts the two agree,
185
185
  // so a bump cannot land with only one of them moved.
186
- const server = new McpServer({ name: "float", version: "0.4.0" });
186
+ const server = new McpServer({ name: "float", version: "0.5.0" });
187
187
  // ----------------------------------------------------------------- reads
188
188
  server.tool("vault_tree", "Read the whole budget tree: every node, its caps, parent, bound agent account, cooldown and freeze state. No key required.", {}, async () => json(await viewFunction(cfg.rpcUrl, requireVault(), "get_tree", {})));
189
189
  server.tool("vault_budget", "Read one node's budget for a UTC day: cap, executed, in-flight (pending) and available, in human units. Omit `day` for today. No key required.", {
@@ -316,6 +316,25 @@ server.tool("vault_create", "For a user who has NO vault yet. Works out where th
316
316
  `the vault will be initialised with "${config.network}".`);
317
317
  }
318
318
  const exists = owner && vaultId ? await accountExists(rpc, vaultId) : false;
319
+ /**
320
+ * ASK THE EXISTING VAULT WHO OWNS IT, rather than inferring it from the
321
+ * derivation that produced the id.
322
+ *
323
+ * `derive_prefix` replaces dots with hyphens and is NOT injective:
324
+ * `a.b.near` and `a-b.near` both derive `a-b-near`, and the factory ships
325
+ * a test asserting exactly that. So "an account exists at your derived id"
326
+ * does not mean "it is yours" — and the message here used to say it did,
327
+ * which would point the loser of a collision at the winner's vault.
328
+ *
329
+ * `null` on any failure, which the planner treats as a THIRD outcome.
330
+ * Collapsing the error into "not yours" would be as wrong as collapsing it
331
+ * into "yours": a vault that is merely unreadable right now is neither.
332
+ */
333
+ const existingOwner = exists && vaultId
334
+ ? await viewFunction(rpc, vaultId, "get_config", {})
335
+ .then((c) => c?.owner ?? null)
336
+ .catch(() => null)
337
+ : null;
319
338
  // AN AGENT NEEDS A VAULT ID, and not for the transaction — the page
320
339
  // derives that from the connected wallet. It is needed for the CREDENTIAL,
321
340
  // which the adapter files under the account the key signs as. A virtual
@@ -345,6 +364,7 @@ server.tool("vault_create", "For a user who has NO vault yet. Works out where th
345
364
  paused: config.paused,
346
365
  appUrl: cfg.appUrl,
347
366
  exists,
367
+ existingOwner,
348
368
  agentPk: generated?.publicKey ?? null,
349
369
  });
350
370
  return json({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floatonnear/mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for the hierarchical budget vault. The agent key signs request_payment only; owner methods emit unsigned payloads for wallet approval.",
6
6
  "license": "MIT",