@floatonnear/mcp 0.6.4 → 0.9.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.
- package/dist/amounts.d.ts +51 -0
- package/dist/amounts.js +55 -0
- package/dist/explain.d.ts +59 -0
- package/dist/explain.js +209 -0
- package/dist/roles.d.ts +3 -0
- package/dist/roles.js +34 -3
- package/dist/server.js +620 -28
- package/dist/token-meta.d.ts +42 -0
- package/dist/token-meta.js +46 -0
- package/package.json +1 -1
package/dist/amounts.d.ts
CHANGED
|
@@ -18,3 +18,54 @@
|
|
|
18
18
|
export declare function toBaseUnits(amount: string, decimals: number): bigint;
|
|
19
19
|
/** Render base units as a canonical human string: no trailing point, no trailing zeroes. */
|
|
20
20
|
export declare function fromBaseUnits(units: bigint, decimals: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* The contract speaks BASE UNITS; a person reading a budget does not.
|
|
23
|
+
*
|
|
24
|
+
* `get_tree` returns `daily_cap: "25000000"` for a 25 USDC cap, while
|
|
25
|
+
* `vault_budget` — reading the same number through `get_node_budget` — returns
|
|
26
|
+
* `"25"`. Two tools, one vault, one figure, two formats, and nothing said
|
|
27
|
+
* which was which. In a demo that reads as a bug even when both are correct,
|
|
28
|
+
* and `"25000000"` next to a `per_tx_cap` of `"10000000"` invites someone to
|
|
29
|
+
* conclude the caps are enormous.
|
|
30
|
+
*
|
|
31
|
+
* CONVERTED ONLY WHERE THE DECIMALS ARE KNOWN. Caps are stored PER TOKEN and a
|
|
32
|
+
* vault may hold limits for several, but this server knows the decimals of
|
|
33
|
+
* exactly one — `NEAR_VAULT_TOKEN_DECIMALS`, which describes
|
|
34
|
+
* `NEAR_VAULT_TOKEN`. Scaling some other token by 6 would print a number that
|
|
35
|
+
* is wrong by orders of magnitude while looking tidy, which is strictly worse
|
|
36
|
+
* than the raw figure it replaced. So a token we cannot vouch for keeps its
|
|
37
|
+
* base units and says so, rather than being guessed at.
|
|
38
|
+
*
|
|
39
|
+
* `decimals` is echoed on every converted entry so the base figure stays
|
|
40
|
+
* derivable; nothing is lost, it is only no longer the default reading.
|
|
41
|
+
*/
|
|
42
|
+
export interface TreeLimit {
|
|
43
|
+
token: string;
|
|
44
|
+
daily_cap: string;
|
|
45
|
+
per_tx_cap: string;
|
|
46
|
+
}
|
|
47
|
+
export interface TreeNode {
|
|
48
|
+
limits?: TreeLimit[];
|
|
49
|
+
child_cap_sum?: [string, string][];
|
|
50
|
+
[k: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
export declare function humaniseTreeNode(node: TreeNode, cfg: {
|
|
53
|
+
token?: string;
|
|
54
|
+
decimals: number;
|
|
55
|
+
/**
|
|
56
|
+
* Per-token metadata, when it could be read from the chain.
|
|
57
|
+
*
|
|
58
|
+
* THIS REMOVES THE CAVEAT THE FIRST VERSION SHIPPED WITH. It could only
|
|
59
|
+
* humanise the one token whose decimals the operator had configured;
|
|
60
|
+
* every other kept base units, because scaling by a guessed exponent
|
|
61
|
+
* prints a figure wrong by orders of magnitude while looking tidy. The
|
|
62
|
+
* chain knows the real answer, so the guess was never necessary.
|
|
63
|
+
*
|
|
64
|
+
* A token still absent from this map keeps base units — "could not read
|
|
65
|
+
* the metadata" is not a licence to assume six.
|
|
66
|
+
*/
|
|
67
|
+
meta?: Map<string, {
|
|
68
|
+
symbol: string;
|
|
69
|
+
decimals: number;
|
|
70
|
+
} | null>;
|
|
71
|
+
}): TreeNode;
|
package/dist/amounts.js
CHANGED
|
@@ -53,3 +53,58 @@ export function fromBaseUnits(units, decimals) {
|
|
|
53
53
|
const frac = (units % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
54
54
|
return frac ? `${whole}.${frac}` : whole.toString();
|
|
55
55
|
}
|
|
56
|
+
export function humaniseTreeNode(node, cfg) {
|
|
57
|
+
if (!node || typeof node !== "object")
|
|
58
|
+
return node;
|
|
59
|
+
// Prefer what the CHAIN said; fall back to the configured token only.
|
|
60
|
+
const dp = (token) => {
|
|
61
|
+
const m = cfg.meta?.get(token);
|
|
62
|
+
if (m)
|
|
63
|
+
return m.decimals;
|
|
64
|
+
return cfg.token && token === cfg.token ? cfg.decimals : null;
|
|
65
|
+
};
|
|
66
|
+
const sym = (token) => cfg.meta?.get(token)?.symbol ?? null;
|
|
67
|
+
const human = (v, decimals) => {
|
|
68
|
+
// EXPLICIT SHAPE CHECK, because `BigInt("")` is `0n` rather than a throw —
|
|
69
|
+
// and `BigInt(" ")` is too. A try/catch alone therefore converted an
|
|
70
|
+
// empty or whitespace cap into "0", which is not a parse failure the
|
|
71
|
+
// reader can see: "0" is a meaningful budget, and the wrong one. Caught by
|
|
72
|
+
// its own test, which is the only reason this is not shipping.
|
|
73
|
+
if (typeof v !== "string" || !/^\d+$/.test(v))
|
|
74
|
+
return v;
|
|
75
|
+
try {
|
|
76
|
+
return fromBaseUnits(BigInt(v), decimals);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Still guarded: fromBaseUnits rejects decimals outside its own range,
|
|
80
|
+
// and a figure the contract produced is a fact worth surfacing
|
|
81
|
+
// unchanged rather than an exception to swallow.
|
|
82
|
+
return v;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const limits = node.limits?.map((l) => {
|
|
86
|
+
const d = dp(l.token);
|
|
87
|
+
const symbol = sym(l.token);
|
|
88
|
+
if (d === null)
|
|
89
|
+
return { ...l, decimals: null, units: "base" };
|
|
90
|
+
return {
|
|
91
|
+
...l,
|
|
92
|
+
// The SYMBOL beside the id, never instead of it: anyone can deploy a
|
|
93
|
+
// token calling itself USDC, and a cap that names only a symbol cannot
|
|
94
|
+
// be checked against the one the user meant.
|
|
95
|
+
...(symbol ? { token_symbol: symbol } : {}),
|
|
96
|
+
daily_cap: human(l.daily_cap, d),
|
|
97
|
+
per_tx_cap: human(l.per_tx_cap, d),
|
|
98
|
+
decimals: d,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
const childCapSum = node.child_cap_sum?.map(([token, amount]) => {
|
|
102
|
+
const d = dp(token);
|
|
103
|
+
return (d === null ? [token, amount] : [token, human(amount, d)]);
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
...node,
|
|
107
|
+
...(limits ? { limits } : {}),
|
|
108
|
+
...(childCapSum ? { child_cap_sum: childCapSum } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a payment outcome into something a person would actually say.
|
|
3
|
+
*
|
|
4
|
+
* WHY THE SERVER DOES THIS AND NOT THE MODEL. Two reasons, and the second is
|
|
5
|
+
* the one that decides it.
|
|
6
|
+
*
|
|
7
|
+
* First, the old instructions literally said "Report the reason code", so the
|
|
8
|
+
* agent did: a user asking to pay someone got `PARENT_PER_TX_LIMIT` back. That
|
|
9
|
+
* is a correct answer to a question nobody asked. Telling the model "be
|
|
10
|
+
* friendly" instead would help, but phrasing drifts between models, between
|
|
11
|
+
* versions, and between one conversation and the next.
|
|
12
|
+
*
|
|
13
|
+
* Second, and decisively: a USEFUL explanation needs NUMBERS the model does
|
|
14
|
+
* not have. "That's over the limit" is nearly as useless as the raw code. What
|
|
15
|
+
* a person needs is "the most you can send in one payment is 10, and this asked
|
|
16
|
+
* for 15 — try 10 or less". Those figures live in the budget tree, which the
|
|
17
|
+
* payment response never carried. So the server reads them and writes the
|
|
18
|
+
* sentence; the agent relays it.
|
|
19
|
+
*
|
|
20
|
+
* THE CODE IS KEPT, not discarded. Precision about WHICH rule fired is a real
|
|
21
|
+
* asset — `PARENT_DAILY_BUDGET` meaning "a sibling drained the shared pool" is
|
|
22
|
+
* something a flat policy model cannot express, and developers and the pitch
|
|
23
|
+
* both want it. So it stays in the payload under `technical`, out of the way
|
|
24
|
+
* of the sentence a user sees.
|
|
25
|
+
*
|
|
26
|
+
* HOUSE STYLE, since this is product copy:
|
|
27
|
+
* - one or two short sentences, no jargon, no ALL-CAPS codes
|
|
28
|
+
* - name the real numbers and the real token symbol
|
|
29
|
+
* - say what to do next when there IS something to do
|
|
30
|
+
* - never imply money moved when it did not, and never imply it is gone
|
|
31
|
+
* when the refusal was free
|
|
32
|
+
*/
|
|
33
|
+
/** What the contract returned, plus whatever context the handler could gather. */
|
|
34
|
+
export interface OutcomeContext {
|
|
35
|
+
status?: string;
|
|
36
|
+
reason_code?: string;
|
|
37
|
+
/** Human-units amount that was requested, e.g. "15". */
|
|
38
|
+
amount: string;
|
|
39
|
+
recipient: string;
|
|
40
|
+
/** Token symbol if known ("USDC"), else a short id. */
|
|
41
|
+
symbol: string;
|
|
42
|
+
/** Per-payment cap in human units, when known. */
|
|
43
|
+
perTxCap?: string | undefined;
|
|
44
|
+
/** Remaining budget today in human units, when known. */
|
|
45
|
+
available?: string | undefined;
|
|
46
|
+
/** Daily cap in human units, when known. */
|
|
47
|
+
dailyCap?: string | undefined;
|
|
48
|
+
/** Seconds left on a cooldown, when known. */
|
|
49
|
+
cooldownLeftSec?: number | undefined;
|
|
50
|
+
}
|
|
51
|
+
export interface Explanation {
|
|
52
|
+
/** The sentence(s) to say to the user. No codes, no jargon. */
|
|
53
|
+
say: string;
|
|
54
|
+
/** A concrete next step, or null when there is nothing for the user to do. */
|
|
55
|
+
next: string | null;
|
|
56
|
+
/** True when value actually moved. */
|
|
57
|
+
moved: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare function explainOutcome(c: OutcomeContext): Explanation;
|
package/dist/explain.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn a payment outcome into something a person would actually say.
|
|
3
|
+
*
|
|
4
|
+
* WHY THE SERVER DOES THIS AND NOT THE MODEL. Two reasons, and the second is
|
|
5
|
+
* the one that decides it.
|
|
6
|
+
*
|
|
7
|
+
* First, the old instructions literally said "Report the reason code", so the
|
|
8
|
+
* agent did: a user asking to pay someone got `PARENT_PER_TX_LIMIT` back. That
|
|
9
|
+
* is a correct answer to a question nobody asked. Telling the model "be
|
|
10
|
+
* friendly" instead would help, but phrasing drifts between models, between
|
|
11
|
+
* versions, and between one conversation and the next.
|
|
12
|
+
*
|
|
13
|
+
* Second, and decisively: a USEFUL explanation needs NUMBERS the model does
|
|
14
|
+
* not have. "That's over the limit" is nearly as useless as the raw code. What
|
|
15
|
+
* a person needs is "the most you can send in one payment is 10, and this asked
|
|
16
|
+
* for 15 — try 10 or less". Those figures live in the budget tree, which the
|
|
17
|
+
* payment response never carried. So the server reads them and writes the
|
|
18
|
+
* sentence; the agent relays it.
|
|
19
|
+
*
|
|
20
|
+
* THE CODE IS KEPT, not discarded. Precision about WHICH rule fired is a real
|
|
21
|
+
* asset — `PARENT_DAILY_BUDGET` meaning "a sibling drained the shared pool" is
|
|
22
|
+
* something a flat policy model cannot express, and developers and the pitch
|
|
23
|
+
* both want it. So it stays in the payload under `technical`, out of the way
|
|
24
|
+
* of the sentence a user sees.
|
|
25
|
+
*
|
|
26
|
+
* HOUSE STYLE, since this is product copy:
|
|
27
|
+
* - one or two short sentences, no jargon, no ALL-CAPS codes
|
|
28
|
+
* - name the real numbers and the real token symbol
|
|
29
|
+
* - say what to do next when there IS something to do
|
|
30
|
+
* - never imply money moved when it did not, and never imply it is gone
|
|
31
|
+
* when the refusal was free
|
|
32
|
+
*/
|
|
33
|
+
const plural = (n, one, many) => (n === 1 ? one : many);
|
|
34
|
+
/** "2 minutes", "3 hours" — a duration a person can act on. */
|
|
35
|
+
function human(sec) {
|
|
36
|
+
if (sec < 60)
|
|
37
|
+
return `${sec} ${plural(sec, "second", "seconds")}`;
|
|
38
|
+
const m = Math.ceil(sec / 60);
|
|
39
|
+
if (m < 60)
|
|
40
|
+
return `${m} ${plural(m, "minute", "minutes")}`;
|
|
41
|
+
const h = Math.ceil(m / 60);
|
|
42
|
+
return `${h} ${plural(h, "hour", "hours")}`;
|
|
43
|
+
}
|
|
44
|
+
export function explainOutcome(c) {
|
|
45
|
+
const amt = `${c.amount} ${c.symbol}`;
|
|
46
|
+
const code = c.reason_code ?? "";
|
|
47
|
+
// ---- It worked. Say what happened, in that order: what, to whom, what is left.
|
|
48
|
+
if (c.status === "SETTLED" || code === "SETTLED") {
|
|
49
|
+
const left = c.available !== undefined
|
|
50
|
+
? ` There's ${c.available} ${c.symbol} left to spend today.`
|
|
51
|
+
: "";
|
|
52
|
+
return { say: `Sent ${amt} to ${c.recipient}.${left}`, next: null, moved: true };
|
|
53
|
+
}
|
|
54
|
+
// ---- In flight. Do NOT claim it arrived.
|
|
55
|
+
if (c.status === "TRANSFER_PENDING" || code === "TRANSFER_PENDING") {
|
|
56
|
+
return {
|
|
57
|
+
say: `The ${amt} payment to ${c.recipient} is on its way — it hasn't confirmed yet.`,
|
|
58
|
+
next: "Check again in a moment to see whether it completed.",
|
|
59
|
+
moved: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* ---- Approved by the rules, then the transfer itself failed.
|
|
64
|
+
*
|
|
65
|
+
* Said carefully: the budget was NOT charged, and people assume a failed
|
|
66
|
+
* payment means missing money. The usual cause is a payee not registered on
|
|
67
|
+
* the token, which is a real thing they can fix.
|
|
68
|
+
*/
|
|
69
|
+
if (c.status === "TRANSFER_FAILED" || code === "TRANSFER_FAILED") {
|
|
70
|
+
return {
|
|
71
|
+
say: `The ${amt} payment to ${c.recipient} didn't go through, so nothing left the vault. ` +
|
|
72
|
+
`Usually that means ${c.recipient} isn't set up to receive ${c.symbol} yet.`,
|
|
73
|
+
next: `Ask ${c.recipient} to register for ${c.symbol}, then try again.`,
|
|
74
|
+
moved: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const nothingMoved = " Nothing was sent.";
|
|
78
|
+
switch (code) {
|
|
79
|
+
// ---- The two limit shapes, which are the ones users actually meet.
|
|
80
|
+
case "PER_TX_LIMIT":
|
|
81
|
+
case "PARENT_PER_TX_LIMIT": {
|
|
82
|
+
const cap = c.perTxCap;
|
|
83
|
+
const whose = code === "PARENT_PER_TX_LIMIT"
|
|
84
|
+
? "a limit set higher up the budget"
|
|
85
|
+
: "this agent's limit";
|
|
86
|
+
return {
|
|
87
|
+
say: cap
|
|
88
|
+
? `That's too big for one payment — ${whose} caps each payment at ${cap} ${c.symbol}, ` +
|
|
89
|
+
`and this asked for ${c.amount}.${nothingMoved}`
|
|
90
|
+
: `That's more than ${whose} allows in a single payment.${nothingMoved}`,
|
|
91
|
+
next: cap
|
|
92
|
+
? `Send ${cap} ${c.symbol} or less, or ask the vault owner to raise the per-payment limit.`
|
|
93
|
+
: "Try a smaller amount, or ask the vault owner to raise the per-payment limit.",
|
|
94
|
+
moved: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
case "DAILY_BUDGET": {
|
|
98
|
+
const left = c.available;
|
|
99
|
+
return {
|
|
100
|
+
say: left
|
|
101
|
+
? `That would go past today's spending limit — ${left} ${c.symbol} is left of ` +
|
|
102
|
+
`${c.dailyCap ?? "today's"} ${c.symbol}, and this asked for ${c.amount}.${nothingMoved}`
|
|
103
|
+
: `Today's spending limit is used up.${nothingMoved}`,
|
|
104
|
+
next: left && Number(left) > 0
|
|
105
|
+
? `Send ${left} ${c.symbol} or less, or wait — the limit resets at midnight UTC.`
|
|
106
|
+
: "The limit resets at midnight UTC, or the owner can raise it.",
|
|
107
|
+
moved: false,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The one worth explaining properly, because the obvious reading is wrong:
|
|
112
|
+
* this agent's OWN allowance can look completely free.
|
|
113
|
+
*/
|
|
114
|
+
case "PARENT_DAILY_BUDGET":
|
|
115
|
+
return {
|
|
116
|
+
say: `This agent still has room, but the shared pool it spends from is used up for today — ` +
|
|
117
|
+
`another agent got there first.${nothingMoved}`,
|
|
118
|
+
next: "The pool resets at midnight UTC, or the owner can raise the shared limit.",
|
|
119
|
+
moved: false,
|
|
120
|
+
};
|
|
121
|
+
case "COOLDOWN":
|
|
122
|
+
case "PARENT_COOLDOWN":
|
|
123
|
+
return {
|
|
124
|
+
say: `Payments from here are spaced out, and the next one isn't due yet` +
|
|
125
|
+
`${c.cooldownLeftSec ? ` — about ${human(c.cooldownLeftSec)} to go` : ""}.${nothingMoved}`,
|
|
126
|
+
next: "Try again shortly.",
|
|
127
|
+
moved: false,
|
|
128
|
+
};
|
|
129
|
+
// ---- Owner decisions. The user cannot fix these themselves, so say who can.
|
|
130
|
+
case "NO_RECIPIENTS_SET":
|
|
131
|
+
return {
|
|
132
|
+
say: `This agent isn't cleared to pay anyone yet.${nothingMoved}`,
|
|
133
|
+
next: "The vault owner needs to approve a list of payees first.",
|
|
134
|
+
moved: false,
|
|
135
|
+
};
|
|
136
|
+
case "RECIPIENT_NOT_ALLOWED":
|
|
137
|
+
return {
|
|
138
|
+
say: `${c.recipient} isn't on this agent's approved list of payees.${nothingMoved}`,
|
|
139
|
+
next: "The vault owner decides who can be paid, and can add them.",
|
|
140
|
+
moved: false,
|
|
141
|
+
};
|
|
142
|
+
case "VAULT_PAUSED":
|
|
143
|
+
return {
|
|
144
|
+
say: `The vault is paused, so no payments can go out right now.${nothingMoved}`,
|
|
145
|
+
next: "The vault owner can unpause it.",
|
|
146
|
+
moved: false,
|
|
147
|
+
};
|
|
148
|
+
case "NODE_FROZEN":
|
|
149
|
+
return {
|
|
150
|
+
say: `This agent is frozen, so it can't spend.${nothingMoved}`,
|
|
151
|
+
next: "The vault owner can unfreeze it.",
|
|
152
|
+
moved: false,
|
|
153
|
+
};
|
|
154
|
+
case "TOKEN_NOT_ALLOWED":
|
|
155
|
+
return {
|
|
156
|
+
say: `This agent doesn't have a ${c.symbol} budget.${nothingMoved}`,
|
|
157
|
+
next: `The vault owner can give it one, or pay in a token it does have.`,
|
|
158
|
+
moved: false,
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* Not a refusal — a pause. Must not read as a failure, and must not read
|
|
162
|
+
* as done either.
|
|
163
|
+
*/
|
|
164
|
+
case "APPROVAL_REQUIRED":
|
|
165
|
+
return {
|
|
166
|
+
say: `${amt} to ${c.recipient} is large enough to need the owner's sign-off, so it's ` +
|
|
167
|
+
`waiting for them. Nothing has been sent yet.`,
|
|
168
|
+
next: "Ask the vault owner to approve it.",
|
|
169
|
+
moved: false,
|
|
170
|
+
};
|
|
171
|
+
case "APPROVAL_REJECTED":
|
|
172
|
+
return {
|
|
173
|
+
say: `The vault owner turned down the ${amt} payment to ${c.recipient}.${nothingMoved}`,
|
|
174
|
+
next: null,
|
|
175
|
+
moved: false,
|
|
176
|
+
};
|
|
177
|
+
case "DUPLICATE_REQUEST":
|
|
178
|
+
return {
|
|
179
|
+
say: "That exact payment was already submitted, so it wasn't sent twice.",
|
|
180
|
+
next: "Check whether the first one went through before trying again.",
|
|
181
|
+
moved: false,
|
|
182
|
+
};
|
|
183
|
+
case "INVALID_AMOUNT":
|
|
184
|
+
return { say: `${c.amount} isn't a valid amount to send.`, next: "Use an amount above zero.", moved: false };
|
|
185
|
+
case "INVALID_MEMO":
|
|
186
|
+
return {
|
|
187
|
+
say: "The note attached to that payment isn't allowed — it's too long or has odd characters.",
|
|
188
|
+
next: "Shorten the note and try again.",
|
|
189
|
+
moved: false,
|
|
190
|
+
};
|
|
191
|
+
default: {
|
|
192
|
+
/**
|
|
193
|
+
* UNKNOWN CODE: say plainly that it was refused and that nothing moved,
|
|
194
|
+
* and THEN give the code — because an unrecognised code is the one case
|
|
195
|
+
* where the raw value is genuinely the most useful thing a user can pass
|
|
196
|
+
* on to whoever can help. Guessing a friendly sentence for a rule we
|
|
197
|
+
* cannot describe would be worse than admitting it.
|
|
198
|
+
*/
|
|
199
|
+
if (!code) {
|
|
200
|
+
return { say: `The ${amt} payment to ${c.recipient} didn't go through.${nothingMoved}`, next: null, moved: false };
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
say: `The payment was refused by the vault's rules, so nothing was sent.`,
|
|
204
|
+
next: `If you need to report this, the reason given was "${code}".`,
|
|
205
|
+
moved: false,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
package/dist/roles.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare class RoleSelectionError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function selectRole(cfg: {
|
|
10
10
|
agentKeysPath?: string;
|
|
11
|
+
vault?: string;
|
|
11
12
|
}, role: string): {
|
|
12
13
|
agentId: string;
|
|
13
14
|
agentKey: string;
|
|
@@ -15,6 +16,7 @@ export declare function selectRole(cfg: {
|
|
|
15
16
|
/** Every role that could actually sign, for listing back to a caller. */
|
|
16
17
|
export declare function listRoles(cfg: {
|
|
17
18
|
agentKeysPath?: string;
|
|
19
|
+
vault?: string;
|
|
18
20
|
}): RoleCredential[];
|
|
19
21
|
/**
|
|
20
22
|
* EVERY credential in the keyring, named or not.
|
|
@@ -36,6 +38,7 @@ export declare function listRoles(cfg: {
|
|
|
36
38
|
*/
|
|
37
39
|
export declare function listAgents(cfg: {
|
|
38
40
|
agentKeysPath?: string;
|
|
41
|
+
vault?: string;
|
|
39
42
|
}): {
|
|
40
43
|
agentId: string;
|
|
41
44
|
role: string | null;
|
package/dist/roles.js
CHANGED
|
@@ -29,11 +29,42 @@ function loadable(path) {
|
|
|
29
29
|
// never be selected.
|
|
30
30
|
return parsed.filter((e) => e && typeof e.agentId === "string" && typeof e.secret === "string");
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Keyring entries belonging to THIS vault.
|
|
34
|
+
*
|
|
35
|
+
* A credential's `agentId` IS the vault account it was issued for. Nothing
|
|
36
|
+
* filtered on it, so every function here operated on the whole file — every
|
|
37
|
+
* vault the user has ever created, on either network.
|
|
38
|
+
*
|
|
39
|
+
* Measured 2026-11-10 against a mainnet vault: `vault_roles` listed
|
|
40
|
+
* `rudeisland4178-testnet.fac.floatv4.testnet` and
|
|
41
|
+
* `work12-testnet-fleet.fac.floatv4.testnet` as that vault's agents. Both are
|
|
42
|
+
* TESTNET accounts from earlier experiments, and neither has any relationship
|
|
43
|
+
* to the configured mainnet vault.
|
|
44
|
+
*
|
|
45
|
+
* `vault_roles` listing them is a display bug. `selectRole` was worse: asked
|
|
46
|
+
* for role "root" it would hand `vault_pay` the testnet fleet key to sign a
|
|
47
|
+
* MAINNET payment. That fails at the chain — the key is not on the vault — but
|
|
48
|
+
* the tool chose a credential from a different network and the error arrives
|
|
49
|
+
* with no hint of why.
|
|
50
|
+
*
|
|
51
|
+
* `vault` empty means nothing is configured, so there is nothing to scope to
|
|
52
|
+
* and every entry is returned. That is the `vault_create` case, where the
|
|
53
|
+
* server legitimately runs with no vault at all.
|
|
54
|
+
*/
|
|
55
|
+
function forVault(cfg) {
|
|
56
|
+
const entries = loadable(cfg.agentKeysPath ?? ".float/agent-keys.json");
|
|
57
|
+
if (!cfg.vault)
|
|
58
|
+
return entries;
|
|
59
|
+
return entries.filter((e) => e.agentId === cfg.vault);
|
|
60
|
+
}
|
|
32
61
|
export class RoleSelectionError extends Error {
|
|
33
62
|
}
|
|
34
63
|
export function selectRole(cfg, role) {
|
|
35
64
|
const path = cfg.agentKeysPath ?? ".float/agent-keys.json";
|
|
36
|
-
|
|
65
|
+
// SCOPED. A role name is not unique across vaults — "root" exists in every
|
|
66
|
+
// one — so an unscoped lookup picks whichever was written first.
|
|
67
|
+
const entries = forVault(cfg);
|
|
37
68
|
const known = entries
|
|
38
69
|
.map((e) => (typeof e.role === "string" ? e.role : null))
|
|
39
70
|
.filter(Boolean);
|
|
@@ -50,7 +81,7 @@ export function selectRole(cfg, role) {
|
|
|
50
81
|
}
|
|
51
82
|
/** Every role that could actually sign, for listing back to a caller. */
|
|
52
83
|
export function listRoles(cfg) {
|
|
53
|
-
return
|
|
84
|
+
return forVault(cfg)
|
|
54
85
|
.filter((e) => typeof e.role === "string")
|
|
55
86
|
.map((e) => ({
|
|
56
87
|
agentId: e.agentId,
|
|
@@ -78,7 +109,7 @@ export function listRoles(cfg) {
|
|
|
78
109
|
* name is genuinely absent and the fix is `vault_add_role`, not a guess.
|
|
79
110
|
*/
|
|
80
111
|
export function listAgents(cfg) {
|
|
81
|
-
return
|
|
112
|
+
return forVault(cfg).map((e) => ({
|
|
82
113
|
agentId: e.agentId,
|
|
83
114
|
role: typeof e.role === "string" ? e.role : null,
|
|
84
115
|
nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
|