@floatonnear/float-adapter 0.2.1 → 0.3.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/blocked-scan.d.ts +78 -0
- package/dist/blocked-scan.js +162 -8
- package/dist/env-file.d.ts +16 -0
- package/dist/env-file.js +16 -0
- package/dist/gate.d.ts +9 -5
- package/dist/gate.js +32 -5
- package/dist/handlers.d.ts +176 -3
- package/dist/handlers.js +262 -29
- package/dist/keyring.d.ts +81 -2
- package/dist/keyring.js +80 -10
- package/dist/map.d.ts +56 -11
- package/dist/map.js +43 -21
- package/dist/owner-plan.d.ts +112 -0
- package/dist/owner-plan.js +112 -0
- package/dist/server.d.ts +10 -1
- package/dist/server.js +461 -53
- package/dist/unavailable.d.ts +31 -0
- package/dist/unavailable.js +90 -0
- package/package.json +2 -2
package/dist/keyring.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* cannot exercise a hierarchy at all; it can only ever spend one leaf.
|
|
15
15
|
*
|
|
16
16
|
* On holding several keys at once: each is the restricted key from
|
|
17
|
-
*
|
|
17
|
+
* a virtual agent's NEP-611 gas key, which can call only `request_payment` on its own
|
|
18
18
|
* vault, cannot call other contracts, and cannot attach a deposit. The vault
|
|
19
19
|
* enforces every cap regardless of who holds the key, so the blast radius of
|
|
20
20
|
* the whole keyring is bounded by the tree's own limits. That is the same
|
|
@@ -25,12 +25,61 @@ export interface AgentCredential {
|
|
|
25
25
|
secret: string;
|
|
26
26
|
/** Present when known; lets a caller select by node instead of account id. */
|
|
27
27
|
nodeId?: number;
|
|
28
|
+
/**
|
|
29
|
+
* A human name for this agent — "marketing", "news".
|
|
30
|
+
*
|
|
31
|
+
* THE ONLY IDENTIFIER THAT IS ACTUALLY UNIQUE PER AGENT once agents are
|
|
32
|
+
* virtual. `agentId` is the account a key signs AS, and a virtual agent's
|
|
33
|
+
* key is a gas key ON THE VAULT, so every agent on one vault shares it.
|
|
34
|
+
* `nodeId` is unique but is a number nobody wants to quote.
|
|
35
|
+
*/
|
|
36
|
+
role?: string;
|
|
28
37
|
}
|
|
29
38
|
export interface Keyring {
|
|
30
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* agent account id -> credential.
|
|
41
|
+
*
|
|
42
|
+
* LOSSY FOR VIRTUAL AGENTS, deliberately kept for compatibility. Every
|
|
43
|
+
* agent on one vault has the same `agentId`, so this map holds only the
|
|
44
|
+
* last one loaded. Use `all` for anything that counts agents and `byRole`
|
|
45
|
+
* or `byNode` to select one.
|
|
46
|
+
*/
|
|
31
47
|
byAccount: Map<string, AgentCredential>;
|
|
32
48
|
/** node id -> credential */
|
|
33
49
|
byNode: Map<number, AgentCredential>;
|
|
50
|
+
/** role name -> credential */
|
|
51
|
+
byRole: Map<string, AgentCredential>;
|
|
52
|
+
/**
|
|
53
|
+
* Every credential loaded, in file order.
|
|
54
|
+
*
|
|
55
|
+
* The ONLY honest count. Ambiguity used to be judged by `byAccount.size`,
|
|
56
|
+
* which collapses virtual agents onto one key — so two agents read as
|
|
57
|
+
* "exactly one, unambiguous" and an un-targeted payment silently signed
|
|
58
|
+
* with whichever was written last, charging the wrong node. Measured
|
|
59
|
+
* 2026-11-03: a two-agent file produced `byAccount.size === 1`.
|
|
60
|
+
*/
|
|
61
|
+
all: AgentCredential[];
|
|
62
|
+
/**
|
|
63
|
+
* How many file entries were skipped because they belong to a different
|
|
64
|
+
* vault. Zero unless `vault` was passed to `loadKeyring`.
|
|
65
|
+
*
|
|
66
|
+
* Surfaced rather than discarded: "no agents configured" and "three agents
|
|
67
|
+
* configured, none of them for this vault" look identical from an empty
|
|
68
|
+
* ring, and the second is a pointing error the operator can fix in
|
|
69
|
+
* seconds once told.
|
|
70
|
+
*/
|
|
71
|
+
skippedOtherVault: number;
|
|
72
|
+
/**
|
|
73
|
+
* Why `NEAR_VAULT_AGENT_KEY_FILE` yielded no key, when one was named.
|
|
74
|
+
*
|
|
75
|
+
* THE THIRD CAUSE OF AN EMPTY RING, and the same reason
|
|
76
|
+
* `skippedOtherVault` is carried rather than discarded. The generic refusal
|
|
77
|
+
* says "Set NEAR_VAULT_AGENT_ID and NEAR_VAULT_AGENT_KEY" — which, for an
|
|
78
|
+
* operator whose key file is simply mistyped, is advice to paste the secret
|
|
79
|
+
* back inline. That is the exposure the file option exists to remove, so
|
|
80
|
+
* misreporting this case actively undoes the change.
|
|
81
|
+
*/
|
|
82
|
+
agentKeyError?: string;
|
|
34
83
|
/**
|
|
35
84
|
* Used when a request names no agent. Preserves the previous single-agent
|
|
36
85
|
* behaviour exactly, so existing deployments and the dashboard's own calls
|
|
@@ -52,6 +101,35 @@ export declare function loadKeyring(env: {
|
|
|
52
101
|
agentId?: string;
|
|
53
102
|
agentKey?: string;
|
|
54
103
|
agentKeysPath?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The vault this adapter serves. When given, credentials belonging to a
|
|
106
|
+
* DIFFERENT vault are skipped.
|
|
107
|
+
*
|
|
108
|
+
* The keyring file is per-machine, not per-vault: one `.float/agent-keys.json`
|
|
109
|
+
* accumulates every agent ever provisioned here. Without this filter an
|
|
110
|
+
* adapter serving vault A also loads vault B's agents, which goes wrong in
|
|
111
|
+
* two ways.
|
|
112
|
+
*
|
|
113
|
+
* Loudly, if there are several: `selectAgent` refuses an un-targeted payment
|
|
114
|
+
* and lists agents that are not even on this vault, so the error names
|
|
115
|
+
* options that could never have worked.
|
|
116
|
+
*
|
|
117
|
+
* QUIETLY, if there is exactly one: `all.length === 1` makes a credential
|
|
118
|
+
* for an unrelated vault the FALLBACK. Every un-targeted payment is then
|
|
119
|
+
* signed by a key the contract has never heard of — it fails on chain with
|
|
120
|
+
* NOT_VIRTUAL_AGENT, which reads like a binding bug rather than a
|
|
121
|
+
* configuration one, and sends the reader looking in the wrong place.
|
|
122
|
+
*
|
|
123
|
+
* Observed 2026-11-03: an adapter on work12-testnet-fleet loaded
|
|
124
|
+
* rudeisland4178's agent and named it in a refusal.
|
|
125
|
+
*/
|
|
126
|
+
vault?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Passed through from `resolveKeySource` when NEAR_VAULT_AGENT_KEY_FILE was
|
|
129
|
+
* named but unreadable. Carried, not acted on: a broken default must not
|
|
130
|
+
* stop the agents that DID load from being selected by name.
|
|
131
|
+
*/
|
|
132
|
+
agentKeyError?: string;
|
|
55
133
|
}): Keyring;
|
|
56
134
|
export declare class AgentSelectionError extends Error {
|
|
57
135
|
}
|
|
@@ -65,4 +143,5 @@ export declare class AgentSelectionError extends Error {
|
|
|
65
143
|
export declare function selectAgent(ring: Keyring, want?: {
|
|
66
144
|
agent?: string;
|
|
67
145
|
nodeId?: number;
|
|
146
|
+
role?: string;
|
|
68
147
|
}): AgentCredential;
|
package/dist/keyring.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* cannot exercise a hierarchy at all; it can only ever spend one leaf.
|
|
15
15
|
*
|
|
16
16
|
* On holding several keys at once: each is the restricted key from
|
|
17
|
-
*
|
|
17
|
+
* a virtual agent's NEP-611 gas key, which can call only `request_payment` on its own
|
|
18
18
|
* vault, cannot call other contracts, and cannot attach a deposit. The vault
|
|
19
19
|
* enforces every cap regardless of who holds the key, so the blast radius of
|
|
20
20
|
* the whole keyring is bounded by the tree's own limits. That is the same
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { readFileSync, existsSync } from "node:fs";
|
|
24
24
|
export function emptyKeyring() {
|
|
25
|
-
return { byAccount: new Map(), byNode: new Map() };
|
|
25
|
+
return { byAccount: new Map(), byNode: new Map(), byRole: new Map(), all: [], skippedOtherVault: 0 };
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Build a keyring from the singular env vars plus, optionally, the JSON file
|
|
@@ -35,10 +35,14 @@ export function emptyKeyring() {
|
|
|
35
35
|
*/
|
|
36
36
|
export function loadKeyring(env) {
|
|
37
37
|
const ring = emptyKeyring();
|
|
38
|
+
ring.agentKeyError = env.agentKeyError;
|
|
38
39
|
const add = (c) => {
|
|
40
|
+
ring.all.push(c);
|
|
39
41
|
ring.byAccount.set(c.agentId, c);
|
|
40
42
|
if (typeof c.nodeId === "number")
|
|
41
43
|
ring.byNode.set(c.nodeId, c);
|
|
44
|
+
if (c.role)
|
|
45
|
+
ring.byRole.set(c.role, c);
|
|
42
46
|
};
|
|
43
47
|
if (env.agentKeysPath && existsSync(env.agentKeysPath)) {
|
|
44
48
|
let parsed;
|
|
@@ -54,10 +58,19 @@ export function loadKeyring(env) {
|
|
|
54
58
|
if (Array.isArray(parsed)) {
|
|
55
59
|
for (const e of parsed) {
|
|
56
60
|
if (e && typeof e.agentId === "string" && typeof e.secret === "string") {
|
|
61
|
+
// A credential for ANOTHER vault is not this adapter's to use.
|
|
62
|
+
// Counted rather than dropped in silence, so a keyring that looks
|
|
63
|
+
// empty can be told apart from one whose entries all belong
|
|
64
|
+
// elsewhere — those are different problems with different fixes.
|
|
65
|
+
if (env.vault && e.agentId !== env.vault) {
|
|
66
|
+
ring.skippedOtherVault += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
57
69
|
add({
|
|
58
70
|
agentId: e.agentId,
|
|
59
71
|
secret: e.secret,
|
|
60
72
|
nodeId: typeof e.nodeId === "number" ? e.nodeId : undefined,
|
|
73
|
+
role: typeof e.role === "string" && e.role ? e.role : undefined,
|
|
61
74
|
});
|
|
62
75
|
}
|
|
63
76
|
}
|
|
@@ -68,11 +81,15 @@ export function loadKeyring(env) {
|
|
|
68
81
|
add(c);
|
|
69
82
|
ring.fallback = c;
|
|
70
83
|
}
|
|
71
|
-
else if (ring.
|
|
72
|
-
// Exactly one
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
84
|
+
else if (ring.all.length === 1) {
|
|
85
|
+
// Exactly one credential and no env pair: unambiguous, so use it.
|
|
86
|
+
//
|
|
87
|
+
// `all.length`, NOT `byAccount.size`. Every virtual agent on a vault
|
|
88
|
+
// shares an `agentId`, so the map collapses them and two agents read as
|
|
89
|
+
// one — which made this branch fire on an ambiguous keyring and pick
|
|
90
|
+
// whichever was written last. Silently: the payment succeeded, against
|
|
91
|
+
// the wrong node's budget.
|
|
92
|
+
ring.fallback = ring.all[0];
|
|
76
93
|
}
|
|
77
94
|
return ring;
|
|
78
95
|
}
|
|
@@ -86,10 +103,26 @@ export class AgentSelectionError extends Error {
|
|
|
86
103
|
* costs the caller another round trip to discover what the valid options were.
|
|
87
104
|
*/
|
|
88
105
|
export function selectAgent(ring, want = {}) {
|
|
106
|
+
// Lists `all`, not `byAccount.values()`. The map holds one entry per
|
|
107
|
+
// ACCOUNT, so on a fleet it named a single agent while several were
|
|
108
|
+
// loaded — an error message that under-reports the thing it exists to
|
|
109
|
+
// report.
|
|
89
110
|
const known = () => {
|
|
90
|
-
const list =
|
|
111
|
+
const list = ring.all.map((c) => {
|
|
112
|
+
const bits = [c.role, typeof c.nodeId === "number" ? `node ${c.nodeId}` : null].filter(Boolean);
|
|
113
|
+
return bits.length ? `${c.agentId} (${bits.join(", ")})` : c.agentId;
|
|
114
|
+
});
|
|
91
115
|
return list.length ? list.join(", ") : "none";
|
|
92
116
|
};
|
|
117
|
+
if (want.role !== undefined) {
|
|
118
|
+
const c = ring.byRole.get(want.role);
|
|
119
|
+
if (!c) {
|
|
120
|
+
const roles = ring.all.map((x) => x.role).filter(Boolean);
|
|
121
|
+
throw new AgentSelectionError(`no key for role '${want.role}'. Known roles: ${roles.length ? roles.join(", ") : "none"}. ` +
|
|
122
|
+
`Loaded agents: ${known()}.`);
|
|
123
|
+
}
|
|
124
|
+
return c;
|
|
125
|
+
}
|
|
93
126
|
if (want.agent !== undefined) {
|
|
94
127
|
const c = ring.byAccount.get(want.agent);
|
|
95
128
|
if (!c) {
|
|
@@ -108,12 +141,49 @@ export function selectAgent(ring, want = {}) {
|
|
|
108
141
|
}
|
|
109
142
|
if (ring.fallback)
|
|
110
143
|
return ring.fallback;
|
|
111
|
-
|
|
144
|
+
// A BROKEN KEY FILE IS WHAT REMOVED THE DEFAULT. Appended to the ambiguity
|
|
145
|
+
// error below as well as replacing the empty-ring one, because with a fleet
|
|
146
|
+
// on disk the ring is NOT empty and "pass role, agent or nodeId" is a true
|
|
147
|
+
// statement that hides a regression — a deployment that had a default and
|
|
148
|
+
// silently lost it reads as a caller bug.
|
|
149
|
+
const keyFileNote = ring.agentKeyError
|
|
150
|
+
? ` The default agent is ALSO missing because a key file was named and could ` +
|
|
151
|
+
`not be read: ${ring.agentKeyError}`
|
|
152
|
+
: "";
|
|
153
|
+
if (ring.all.length > 1) {
|
|
112
154
|
// Refuse rather than guess. Picking arbitrarily would charge a budget the
|
|
113
155
|
// caller did not choose, and the payment would succeed — so the mistake
|
|
114
156
|
// would surface later as a confusing balance, not as an error.
|
|
157
|
+
//
|
|
158
|
+
// `all.length`, NOT `byAccount.size`. This guard was unreachable for a
|
|
159
|
+
// fleet of virtual agents: they share an `agentId`, the map held one, and
|
|
160
|
+
// `loadKeyring` had already installed the last-written one as `fallback`
|
|
161
|
+
// on the way past. The refusal below never fired and the wrong agent paid.
|
|
115
162
|
throw new AgentSelectionError(`several agents are loaded and the request named none, so there is no ` +
|
|
116
|
-
`safe default. Pass "agent" or "nodeId". Loaded agents: ${known()}.`
|
|
163
|
+
`safe default. Pass "role", "agent" or "nodeId". Loaded agents: ${known()}.` +
|
|
164
|
+
keyFileNote);
|
|
165
|
+
}
|
|
166
|
+
// BEFORE the other-vault and nothing-configured branches. A named-but-broken
|
|
167
|
+
// key file is a cause; both of those are descriptions of an empty result, and
|
|
168
|
+
// the nothing-configured one would tell the operator to paste the secret
|
|
169
|
+
// inline — undoing the reason the file option exists.
|
|
170
|
+
if (ring.agentKeyError) {
|
|
171
|
+
throw new AgentSelectionError(`no agent key could be loaded: ${ring.agentKeyError} Fix the path or the file — ` +
|
|
172
|
+
`or unset the _FILE variable to fall back to an inline NEAR_VAULT_AGENT_KEY.` +
|
|
173
|
+
(ring.skippedOtherVault > 0
|
|
174
|
+
? ` (${ring.skippedOtherVault} credential(s) in the keyring file belong to a ` +
|
|
175
|
+
`different vault and were skipped.)`
|
|
176
|
+
: ""));
|
|
177
|
+
}
|
|
178
|
+
// "NOTHING CONFIGURED" AND "NOTHING FOR THIS VAULT" ARE DIFFERENT PROBLEMS.
|
|
179
|
+
// Both leave an empty ring, and the generic message sends someone to add
|
|
180
|
+
// credentials they already have — when the real fix is pointing the adapter
|
|
181
|
+
// at the vault those credentials belong to.
|
|
182
|
+
if (ring.skippedOtherVault > 0) {
|
|
183
|
+
throw new AgentSelectionError(`no agent credentials for this vault. ${ring.skippedOtherVault} credential(s) ` +
|
|
184
|
+
`were loaded but belong to a DIFFERENT vault, so they were skipped — a key ` +
|
|
185
|
+
`bound to another vault cannot spend here. Check NEAR_VAULT_CONTRACT points ` +
|
|
186
|
+
`at the vault you mean, or provision an agent for it with vault_add_role.`);
|
|
117
187
|
}
|
|
118
188
|
throw new AgentSelectionError("no agent credentials are configured. Set NEAR_VAULT_AGENT_ID and " +
|
|
119
189
|
"NEAR_VAULT_AGENT_KEY, or point NEAR_VAULT_AGENT_KEYS at the " +
|
package/dist/map.d.ts
CHANGED
|
@@ -69,9 +69,47 @@ export interface DayUsage {
|
|
|
69
69
|
export interface ApiAgent {
|
|
70
70
|
agentId: string;
|
|
71
71
|
walletAddress: string;
|
|
72
|
-
|
|
72
|
+
/**
|
|
73
|
+
* NULL, because the contract does not record one.
|
|
74
|
+
*
|
|
75
|
+
* `Node` has no timestamp field - id, parent, path, label, account,
|
|
76
|
+
* limits, cooldown, frozen, recipients, and nothing else. Only a payment
|
|
77
|
+
* REQUEST carries `created_at_ns`.
|
|
78
|
+
*
|
|
79
|
+
* This used to be `new Date(0).toISOString()`, which satisfied a
|
|
80
|
+
* non-nullable type by inventing "1970-01-01T00:00:00.000Z". Nothing
|
|
81
|
+
* rendered it yet, so it was not a visible bug - it was a loaded one: the
|
|
82
|
+
* first page to show an agent's age would have shipped "Created Jan 1,
|
|
83
|
+
* 1970" with complete confidence. A missing value has to BE missing, or
|
|
84
|
+
* the type system is helping you lie.
|
|
85
|
+
*/
|
|
86
|
+
createdAt: string | null;
|
|
73
87
|
status: "active" | "killed";
|
|
88
|
+
/** The node's own label. NO tree glyphs, no indentation. */
|
|
74
89
|
displayName: string;
|
|
90
|
+
/**
|
|
91
|
+
* How deep in the budget tree this node sits. Root is 0.
|
|
92
|
+
*
|
|
93
|
+
* A NUMBER, deliberately. Depth used to be encoded into displayName as
|
|
94
|
+
* U+00A0 padding plus a box-drawing glyph, and every invisible-character
|
|
95
|
+
* bug in this project traces back to that: regexes that silently matched
|
|
96
|
+
* nothing, `btoa` throwing on the glyph, agent names carrying invisible
|
|
97
|
+
* padding into prose. A consumer that wants indentation can multiply.
|
|
98
|
+
*/
|
|
99
|
+
depth: number;
|
|
100
|
+
/**
|
|
101
|
+
* This node's parent id, or null at the root.
|
|
102
|
+
*
|
|
103
|
+
* WITHOUT IT THE DASHBOARD CANNOT CHECK THE CONTRACT'S OWN RULE. Under
|
|
104
|
+
* strict allocation `set_node_caps` refuses a cap below the sum already
|
|
105
|
+
* allocated to that node's children, at EVERY depth, not just root
|
|
106
|
+
* (lib.rs:854). The policies page could only evaluate that for root,
|
|
107
|
+
* because `depth` alone says a node sits at level 2 but not WHICH level-1
|
|
108
|
+
* node it hangs from. So shrinking a mid-tree budget below its children
|
|
109
|
+
* passed validation, and the owner signed and paid gas for a transaction
|
|
110
|
+
* the contract was always going to reject.
|
|
111
|
+
*/
|
|
112
|
+
parent: number | null;
|
|
75
113
|
lifetimeSpent?: string;
|
|
76
114
|
lifetimePayments?: number;
|
|
77
115
|
walletBalance?: string | null;
|
|
@@ -82,7 +120,24 @@ export interface ApiAgent {
|
|
|
82
120
|
perTxLimit?: string;
|
|
83
121
|
cooldownSeconds?: number;
|
|
84
122
|
blockUnknown?: boolean;
|
|
123
|
+
/** The EFFECTIVE allowlist — this node's own, or the nearest ancestor's. */
|
|
85
124
|
allowedAddresses?: string[];
|
|
125
|
+
/**
|
|
126
|
+
* This node's OWN list, present only when it has one.
|
|
127
|
+
*
|
|
128
|
+
* Reported separately from `allowedAddresses` because the two answer
|
|
129
|
+
* different questions, and an editor needs both. `allowedAddresses` says
|
|
130
|
+
* "who can be paid from here", which is what a reader wants. This says
|
|
131
|
+
* "who is written on THIS node", which is what a writer needs — because
|
|
132
|
+
* saving a list REPLACES the inherited one rather than extending it, so
|
|
133
|
+
* an editor that only sees the effective list would show a parent's
|
|
134
|
+
* addresses and silently offer to copy them onto the child.
|
|
135
|
+
*
|
|
136
|
+
* Absent means inheriting. That is a different state from an empty list,
|
|
137
|
+
* which the contract rejects outright: `validate_recipients` fails on
|
|
138
|
+
* `is_empty()` with "1..=32 recipients, or None to inherit".
|
|
139
|
+
*/
|
|
140
|
+
ownAllowedAddresses?: string[];
|
|
86
141
|
} | null;
|
|
87
142
|
}
|
|
88
143
|
export interface Payment {
|
|
@@ -113,16 +168,6 @@ export declare function treeToApiAgents(tree: VaultNode[], budgets: Map<number,
|
|
|
113
168
|
spent: bigint;
|
|
114
169
|
count: number;
|
|
115
170
|
}>): ApiAgent[];
|
|
116
|
-
/**
|
|
117
|
-
* Depth prefix using NON-BREAKING spaces.
|
|
118
|
-
*
|
|
119
|
-
* Plain spaces look right in a test assertion and vanish on screen: HTML
|
|
120
|
-
* collapses leading whitespace, so depth-2 rendered identically to depth-1 and
|
|
121
|
-
* the hierarchy — the entire point of the view — was invisible. The unit test
|
|
122
|
-
* was green while the screen was wrong, which is why this is asserted against
|
|
123
|
-
* the rendered character and not a trimmed string.
|
|
124
|
-
*/
|
|
125
|
-
export declare function indent(depth: number, label: string): string;
|
|
126
171
|
/** Mirrors the contract's `effective_recipients`: nearest ancestor with a list. */
|
|
127
172
|
export declare function effectiveRecipients(n: VaultNode, byId: Map<number, VaultNode>): string[] | undefined;
|
|
128
173
|
/** Settled spend per node, within RETAINED history (purge_settled removes rows). */
|
package/dist/map.js
CHANGED
|
@@ -23,9 +23,11 @@ export function nodeToApiAgent(n, usage, decimals) {
|
|
|
23
23
|
// the thing this product exists to make visible.
|
|
24
24
|
agentId: n.account ?? `node-${n.id}`,
|
|
25
25
|
walletAddress: n.account ?? "",
|
|
26
|
-
createdAt:
|
|
26
|
+
createdAt: null,
|
|
27
27
|
status: n.frozen ? "killed" : "active",
|
|
28
28
|
displayName: n.label,
|
|
29
|
+
depth: 0,
|
|
30
|
+
parent: n.parent,
|
|
29
31
|
dailySpent: fromBaseUnits(used, decimals),
|
|
30
32
|
dailyBudgetUsedPercent: cap > 0n ? Number((used * 100n) / cap) : 0,
|
|
31
33
|
walletBalance: null, // the VAULT holds the tokens; an agent account holds none
|
|
@@ -50,12 +52,21 @@ export function treeToApiAgents(tree, budgets, decimals, lifetime = new Map()) {
|
|
|
50
52
|
const byId = new Map(tree.map((n) => [n.id, n]));
|
|
51
53
|
return tree.map((n) => {
|
|
52
54
|
const a = nodeToApiAgent(n, budgets.get(n.id) ?? { executed: "0", pending: "0" }, decimals);
|
|
53
|
-
a.
|
|
55
|
+
a.depth = n.path.length;
|
|
54
56
|
// The EFFECTIVE allowlist, resolved the way the contract resolves it.
|
|
55
57
|
// Reporting the raw `recipients` field made every inheriting node look
|
|
56
58
|
// unrestricted in the UI ("Open x402"), which is the opposite of true:
|
|
57
59
|
// they inherit the root's allowlist and the contract enforces it.
|
|
58
|
-
|
|
60
|
+
// BOTH, deliberately. `allowedAddresses` is what the contract will
|
|
61
|
+
// enforce here; `ownAllowedAddresses` is what is actually written on this
|
|
62
|
+
// node, and is undefined when it inherits. An editor that cannot tell
|
|
63
|
+
// them apart will offer to "keep" a parent's list and, by saving it,
|
|
64
|
+
// detach the child from the parent forever.
|
|
65
|
+
a.policy = {
|
|
66
|
+
...a.policy,
|
|
67
|
+
allowedAddresses: effectiveRecipients(n, byId),
|
|
68
|
+
ownAllowedAddresses: n.recipients?.length ? n.recipients : undefined,
|
|
69
|
+
};
|
|
59
70
|
const lt = lifetime.get(n.id);
|
|
60
71
|
if (lt) {
|
|
61
72
|
a.lifetimeSpent = fromBaseUnits(lt.spent, decimals);
|
|
@@ -64,20 +75,6 @@ export function treeToApiAgents(tree, budgets, decimals, lifetime = new Map()) {
|
|
|
64
75
|
return a;
|
|
65
76
|
});
|
|
66
77
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Depth prefix using NON-BREAKING spaces.
|
|
69
|
-
*
|
|
70
|
-
* Plain spaces look right in a test assertion and vanish on screen: HTML
|
|
71
|
-
* collapses leading whitespace, so depth-2 rendered identically to depth-1 and
|
|
72
|
-
* the hierarchy — the entire point of the view — was invisible. The unit test
|
|
73
|
-
* was green while the screen was wrong, which is why this is asserted against
|
|
74
|
-
* the rendered character and not a trimmed string.
|
|
75
|
-
*/
|
|
76
|
-
export function indent(depth, label) {
|
|
77
|
-
if (depth === 0)
|
|
78
|
-
return label;
|
|
79
|
-
return `${"\u00a0".repeat(3 * (depth - 1))}└─ ${label}`;
|
|
80
|
-
}
|
|
81
78
|
/** Mirrors the contract's `effective_recipients`: nearest ancestor with a list. */
|
|
82
79
|
export function effectiveRecipients(n, byId) {
|
|
83
80
|
const chain = [...n.path, n.id].reverse(); // self first, then upward
|
|
@@ -138,9 +135,30 @@ export function requestToPayment(r, nodeLabels, decimals, delivery) {
|
|
|
138
135
|
amount: fromBaseUnits(BigInt(r.amount), decimals),
|
|
139
136
|
currency: "USDC",
|
|
140
137
|
recipientAddress: r.recipient,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
/**
|
|
139
|
+
* THE MEMO IS REDACTED HERE, and only here.
|
|
140
|
+
*
|
|
141
|
+
* The dashboard is keyless and multi-tenant: anyone can type a vault name
|
|
142
|
+
* and read it. A memo is what the agent paid FOR — "landing page draft",
|
|
143
|
+
* "copywriting" — and that is a company's operations, legible to anyone
|
|
144
|
+
* who guesses an account id.
|
|
145
|
+
*
|
|
146
|
+
* BE EXACT ABOUT WHAT THIS DOES. The memo is ON CHAIN and public; this
|
|
147
|
+
* does not make it secret and must never be described as privacy. It
|
|
148
|
+
* removes it from the surface that turned "know how to query a NEAR RPC"
|
|
149
|
+
* into "type a name", which is a real reduction in casual exposure and
|
|
150
|
+
* nothing more. Anyone determined still has it in one view call.
|
|
151
|
+
*
|
|
152
|
+
* THE AGENT'S OWN TOOLING IS UNAFFECTED. `vault_history` in
|
|
153
|
+
* @floatonnear/mcp calls `list_requests` directly against the chain and
|
|
154
|
+
* never passes through this adapter, so an agent keeps full context on the
|
|
155
|
+
* machine its owner controls. The split is PUBLIC SURFACE vs LOCAL TOOL,
|
|
156
|
+
* not two different truths.
|
|
157
|
+
*
|
|
158
|
+
* Falls back to the recipient, which was already the behaviour for a
|
|
159
|
+
* payment with no memo — so a row still says who was paid.
|
|
160
|
+
*/
|
|
161
|
+
venue: r.recipient,
|
|
144
162
|
rail: railOf(r.route),
|
|
145
163
|
// A Base payment is NOT "success". The contract proved it obtained a
|
|
146
164
|
// signature; it cannot see whether the transaction landed, and nothing in
|
|
@@ -161,7 +179,11 @@ export function requestToPayment(r, nodeLabels, decimals, delivery) {
|
|
|
161
179
|
// anything undelivered.
|
|
162
180
|
txSignature: delivery?.baseTxHash ?? null,
|
|
163
181
|
createdAt: nsToIso(r.created_at_ns),
|
|
164
|
-
|
|
182
|
+
// Redacted with `venue` above. Null rather than omitted, so a consumer
|
|
183
|
+
// that expects the field still type-checks and an absent memo and a
|
|
184
|
+
// withheld one look the same to it — the dashboard must not render a
|
|
185
|
+
// "redacted" badge that advertises which vaults have interesting ones.
|
|
186
|
+
prompt: null,
|
|
165
187
|
resultUrl: null,
|
|
166
188
|
// Carried for terminal failures too, so a failed row can say WHY.
|
|
167
189
|
blockReason: r.status === "SETTLED" ? null : r.reason_code,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every owner action the dashboard can prepare.
|
|
3
|
+
*
|
|
4
|
+
* This lived as a switch inline in the request handler, which meant it was
|
|
5
|
+
* unreachable from a test — and that is how `pause` and `allocation` came to
|
|
6
|
+
* be rendered as dead readout text on the Policies page despite both having
|
|
7
|
+
* contract setters and both being checked on every single payment.
|
|
8
|
+
*
|
|
9
|
+
* It stays a PURE function: no signing, no network, no key. The adapter only
|
|
10
|
+
* ever holds an agent key, which can call `request_payment` and nothing else,
|
|
11
|
+
* so an owner method can only ever be *described* here and signed in the
|
|
12
|
+
* owner's own wallet.
|
|
13
|
+
*/
|
|
14
|
+
export type OwnerPlanRequest = {
|
|
15
|
+
action: "caps";
|
|
16
|
+
nodeId: number;
|
|
17
|
+
dailyCap?: string;
|
|
18
|
+
perTxCap?: string;
|
|
19
|
+
} | {
|
|
20
|
+
action: "cooldown";
|
|
21
|
+
nodeId: number;
|
|
22
|
+
cooldownSec?: number;
|
|
23
|
+
} | {
|
|
24
|
+
action: "recipients";
|
|
25
|
+
nodeId: number;
|
|
26
|
+
recipients?: string[] | null;
|
|
27
|
+
} | {
|
|
28
|
+
action: "freeze";
|
|
29
|
+
nodeId: number;
|
|
30
|
+
frozen?: boolean;
|
|
31
|
+
} | {
|
|
32
|
+
action: "pause";
|
|
33
|
+
paused?: boolean;
|
|
34
|
+
} | {
|
|
35
|
+
action: "allocation";
|
|
36
|
+
strict?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The two teardown steps a BROWSER SIGNATURE CAN REACH.
|
|
40
|
+
*
|
|
41
|
+
* Closing a vault is six steps (see scripts/teardown-vault.mjs) and four of
|
|
42
|
+
* them need the VAULT as signer, not the owner: the gas-key withdrawal, the
|
|
43
|
+
* token's `storage_unregister`, the 33-byte stub deploy and the
|
|
44
|
+
* `DeleteAccount` itself. A wallet refuses `DeployContract` from a dApp —
|
|
45
|
+
* which is the entire reason vault CREATION goes through a factory — so a
|
|
46
|
+
* dashboard cannot finish a teardown and must not pretend to.
|
|
47
|
+
*
|
|
48
|
+
* These two it can do, and they are the two that matter most:
|
|
49
|
+
*
|
|
50
|
+
* withdraw deleting an account does NOT return fungible tokens, so
|
|
51
|
+
* moving them out first is the difference between recovering
|
|
52
|
+
* a balance and burning it
|
|
53
|
+
* purge the only lever that shrinks STATE, and without it the
|
|
54
|
+
* delete is refused outright
|
|
55
|
+
*/
|
|
56
|
+
| {
|
|
57
|
+
action: "withdraw";
|
|
58
|
+
token?: string;
|
|
59
|
+
amountBase?: string;
|
|
60
|
+
receiverId?: string;
|
|
61
|
+
} | {
|
|
62
|
+
action: "purge";
|
|
63
|
+
beforeDay?: number;
|
|
64
|
+
limit?: number;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Give an agent key spending authority, AFTER the vault already exists.
|
|
68
|
+
*
|
|
69
|
+
* `add_role` is the method and `bind_virtual_agent` is not, which is not
|
|
70
|
+
* obvious and matters. Binding alone grants authority and leaves the key
|
|
71
|
+
* unable to transact: it still needs installing as a NEP-611 gas key ON
|
|
72
|
+
* the vault, and that `AddKey` would need the VAULT as signer — the same
|
|
73
|
+
* wall the teardown runs into.
|
|
74
|
+
*
|
|
75
|
+
* `add_role` does the whole thing in one owner-signed call, and the
|
|
76
|
+
* contract says why in its own comment: "THE KEY IS ADDED TO THIS
|
|
77
|
+
* ACCOUNT, BY THIS ACCOUNT. That is the only way an AddKey can reach the
|
|
78
|
+
* vault from a browser signature." The attached deposit funds the gas key
|
|
79
|
+
* and the vault nets zero.
|
|
80
|
+
*
|
|
81
|
+
* The cost is that it creates a NEW node under root rather than attaching
|
|
82
|
+
* to an existing one. That is the honest trade: the alternative is not
|
|
83
|
+
* "attach to an existing node", it is "cannot be done from a browser".
|
|
84
|
+
*/
|
|
85
|
+
| {
|
|
86
|
+
action: "role";
|
|
87
|
+
label?: string;
|
|
88
|
+
dailyCap?: string;
|
|
89
|
+
perTxCap?: string;
|
|
90
|
+
cooldownSec?: number;
|
|
91
|
+
recipients?: string[] | null;
|
|
92
|
+
publicKey?: string;
|
|
93
|
+
fundingYocto?: string;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The action name is not one we serve.
|
|
97
|
+
*
|
|
98
|
+
* A SEPARATE type from a bad argument, because the HTTP layer has to tell
|
|
99
|
+
* them apart: it used to catch every throw from here and report
|
|
100
|
+
* `unknown action: <name>`, so a negative cooldown or a duplicate recipient
|
|
101
|
+
* was reported as a method that does not exist - the one explanation that
|
|
102
|
+
* cannot lead the caller to a fix.
|
|
103
|
+
*/
|
|
104
|
+
export declare class UnknownActionError extends Error {
|
|
105
|
+
constructor(action: string);
|
|
106
|
+
}
|
|
107
|
+
export type OwnerPlanEnv = {
|
|
108
|
+
vault: string;
|
|
109
|
+
token: string;
|
|
110
|
+
decimals: number;
|
|
111
|
+
};
|
|
112
|
+
export declare function planOwnerAction(b: OwnerPlanRequest, env: OwnerPlanEnv): import("@floatonnear/mcp/payloads").OwnerPayload;
|