@dfm-fi/agent 0.2.1212 → 0.2.1423
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 +14 -20
- package/dist/api-client.d.ts +29 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +22 -0
- package/dist/api-client.js.map +1 -1
- package/dist/create-flow.d.ts +6 -0
- package/dist/create-flow.d.ts.map +1 -1
- package/dist/create-flow.js.map +1 -1
- package/dist/keyless.d.ts +10 -32
- package/dist/keyless.d.ts.map +1 -1
- package/dist/keyless.js +13 -35
- package/dist/keyless.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +104 -259
- package/dist/mcp-server.js.map +1 -1
- package/dist/who-pays.test.js +51 -36
- package/dist/who-pays.test.js.map +1 -1
- package/package.json +1 -1
- package/skills.md +7 -5
package/dist/mcp-server.js
CHANGED
|
@@ -54,11 +54,8 @@ import { z } from 'zod';
|
|
|
54
54
|
import { loadConfig } from './config.js';
|
|
55
55
|
import { SiwsSession } from './session.js';
|
|
56
56
|
import { DfmApiClient } from './api-client.js';
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
59
|
-
import { isKeylessMode, shouldExecuteViaAppWallet, KEYLESS_SETUP_HINT, } from './keyless.js';
|
|
60
|
-
import { runCancel, runUpdateEnvelope } from './recovery-flow.js';
|
|
61
|
-
import { runTransferAdmin, runInitRebalancer, runRebalanceToTargets, runUpdateVaultAssets, runAcceptAdminTransfer, runUpdateVaultPolicy, reconcileKeylessUpdateVaultPolicy, } from './manager-flow.js';
|
|
57
|
+
import { isKeylessMode, shouldExecuteViaAppWallet, executeAppWalletWrite, KEYLESS_SETUP_HINT, } from './keyless.js';
|
|
58
|
+
import { runAcceptAdminTransfer, runUpdateVaultPolicy, reconcileKeylessUpdateVaultPolicy, } from './manager-flow.js';
|
|
62
59
|
import { submitSignedTx } from './submit.js';
|
|
63
60
|
import { signBase64Transaction } from './signing.js';
|
|
64
61
|
import { buildWalletIdentity, resolveZapStatusWallet, withActingWalletAlias, withPortfolioWalletAlias, } from './wallet-identity.js';
|
|
@@ -230,7 +227,14 @@ server.registerTool('discover_tokens', {
|
|
|
230
227
|
assetClass: z
|
|
231
228
|
.string()
|
|
232
229
|
.optional()
|
|
233
|
-
|
|
230
|
+
// Was "LST | Spot | FUN" — three of the six the API accepts, so an
|
|
231
|
+
// external agent could not narrow to stablecoins, yield-bearing
|
|
232
|
+
// tokens or tokenized RWAs even though `?class=` has taken all three
|
|
233
|
+
// for months. Same list Kim's in-app registry offers, deliberately:
|
|
234
|
+
// two agent surfaces answering the same question differently is the
|
|
235
|
+
// drift this alignment pass exists to remove.
|
|
236
|
+
.describe('Optional filter: lst (liquid staking) | spot (regular tokens) | ' +
|
|
237
|
+
'fun (pump.fun) | stable | ybt (yield-bearing) | rwa (tokenized real-world assets)'),
|
|
234
238
|
},
|
|
235
239
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
236
240
|
}, async ({ query, assetClass }) => {
|
|
@@ -515,15 +519,10 @@ function writeGate() {
|
|
|
515
519
|
* to install the very keypair the credential model exists to avoid.
|
|
516
520
|
*/
|
|
517
521
|
function keylessUnsupported(tool) {
|
|
518
|
-
|
|
522
|
+
// Explicit owner-signed top-ups are the sole transaction-signing exception.
|
|
523
|
+
if (tool === 'app_wallet_fund' && !isKeylessMode(config))
|
|
519
524
|
return null;
|
|
520
|
-
return errText(`${tool}
|
|
521
|
-
`It has no server-side autonomous path yet, so there is nothing here that can ` +
|
|
522
|
-
`sign it. Keyless writes today: deposit, redeem, launch_vault, update_vault_assets, ` +
|
|
523
|
-
`update_vault_policy, rebalance_to_targets, zap_v2_cancel, ` +
|
|
524
|
-
`zap_v2_update_envelope. ` +
|
|
525
|
-
`For this one, either run the MCP with a local keypair, or drive it from the web app ` +
|
|
526
|
-
`where you sign with your own wallet.`);
|
|
525
|
+
return errText(`${tool} has no app-wallet signing path in this agent. Local signing cannot override that requirement.`);
|
|
527
526
|
}
|
|
528
527
|
/**
|
|
529
528
|
* Resolve both wallet roles from the server on each call. The app wallet may
|
|
@@ -540,49 +539,13 @@ async function resolveWalletIdentity() {
|
|
|
540
539
|
return buildWalletIdentity(null, localSignIn);
|
|
541
540
|
}
|
|
542
541
|
}
|
|
543
|
-
/**
|
|
544
|
-
* The app wallet's status, read FRESH on every ask.
|
|
545
|
-
*
|
|
546
|
-
* It used to be cached for the process lifetime, on the reasoning that the
|
|
547
|
-
* answer "cannot change mid-session without the operator restarting". This
|
|
548
|
-
* same server exposes `app_wallet_rearm`, so it plainly can: read `revoked`,
|
|
549
|
-
* re-arm, and the cache keeps serving the stale value while the new wallet sits
|
|
550
|
-
* ACTIVE — every later write falls to the local signer until a restart. Under
|
|
551
|
-
* WHO PAYS that is not a missed optimisation, it is signing with the wrong
|
|
552
|
-
* wallet, and the chain rejects it because the app wallet owns the escrow.
|
|
553
|
-
*
|
|
554
|
-
* One extra round trip per money write is the correct price.
|
|
555
|
-
*
|
|
556
|
-
* It does NOT swallow a failed lookup. There is a real difference between "the
|
|
557
|
-
* server answered, and there is no app wallet" (null → sign locally) and "I
|
|
558
|
-
* could not find out who pays" (throw). Mapping the second onto the first —
|
|
559
|
-
* which a `.catch(() => null)` here did — means a 500 or a timeout silently
|
|
560
|
-
* re-selects the external wallet in the mixed setup: the exact bug this rule
|
|
561
|
-
* exists to close, reappearing on a bad network moment. The callers all sit
|
|
562
|
-
* inside a try/catch that reports the failure, which is the right outcome:
|
|
563
|
-
* refuse to guess rather than sign with a wallet chosen blind.
|
|
564
|
-
*/
|
|
565
|
-
function readAppWalletStatus() {
|
|
566
|
-
return api.getOperatingWallet().then((op) => op?.status ?? null);
|
|
567
|
-
}
|
|
568
542
|
async function keylessWrite(tool, args) {
|
|
569
|
-
// WHO PAYS. Not "is this process keyless" any more: an active app wallet is
|
|
570
|
-
// the lane even when a local keypair is present, because the keypair wallet is
|
|
571
|
-
// not where the user's money is. See keyless.ts for the mainnet measurement
|
|
572
|
-
// that forced this.
|
|
573
|
-
const viaAppWallet = shouldExecuteViaAppWallet(config, await readAppWalletStatus());
|
|
574
|
-
if (!viaAppWallet)
|
|
575
|
-
return null;
|
|
576
543
|
try {
|
|
577
|
-
|
|
578
|
-
return res ? text(res) : null;
|
|
544
|
+
return text(await executeAppWalletWrite(config, api, tool, args));
|
|
579
545
|
}
|
|
580
546
|
catch (e) {
|
|
581
547
|
const message = e.message;
|
|
582
|
-
|
|
583
|
-
// wallet. Only append the setup checklist when the failure actually points
|
|
584
|
-
// at auth, custody, activation, or permission setup.
|
|
585
|
-
const setupRelevant = /DFM API (401|403|409|501)\b|operating wallet|read-only|allowedOps|custody unavailable/i.test(message);
|
|
548
|
+
const setupRelevant = /OPERATING_WALLET_REQUIRED|DFM API (401|403|409|501)\b|operating wallet|read-only|allowedOps|custody unavailable/i.test(message);
|
|
586
549
|
return errText(`${tool} failed (app wallet): ${message}${setupRelevant ? `\n${KEYLESS_SETUP_HINT}` : ''}`);
|
|
587
550
|
}
|
|
588
551
|
}
|
|
@@ -592,7 +555,11 @@ server.registerTool('launch_vault', {
|
|
|
592
555
|
description: 'REAL-MONEY (gated). Create a new DTF vault. WHO PAYS: your APP WALLET ' +
|
|
593
556
|
'(the Privy operating wallet) signs and funds this whenever you have an ' +
|
|
594
557
|
'active one — that is where your money is, and it becomes the vault admin. ' +
|
|
595
|
-
'
|
|
558
|
+
'An app wallet is REQUIRED here: with no active one this tool refuses ' +
|
|
559
|
+
'instead of signing with a local keypair, because the creator is fixed ' +
|
|
560
|
+
'on-chain forever and is what registers the fund to you — a launch signed ' +
|
|
561
|
+
'by any other wallet is a fund nobody can claim. Anyone can get an app ' +
|
|
562
|
+
'wallet (no allowlist); this is an identity rule, not a permission one. Check ' +
|
|
596
563
|
'`app_wallet` first if a write is refused for funds. `assets` is the basket — each ' +
|
|
597
564
|
'{ mint, symbol, allocationBps }; allocationBps MUST sum to exactly 10000, ' +
|
|
598
565
|
'≤10 assets (mainnet cap). Live factory/fund-tier rules decide mint eligibility; ' +
|
|
@@ -600,7 +567,9 @@ server.registerTool('launch_vault', {
|
|
|
600
567
|
'and read live by DFM — the creator cannot set them. After the vault is recorded, ' +
|
|
601
568
|
'the share mint automatically gets ' +
|
|
602
569
|
'Metaplex token metadata (name/symbol/uri) so explorers + wallets show the ' +
|
|
603
|
-
'DTF instead of "Unknown Token" (#47, best-effort + non-fatal)
|
|
570
|
+
'DTF instead of "Unknown Token" (#47, best-effort + non-fatal); pass `metadata:false` ' +
|
|
571
|
+
'to skip that tx (~0.015 SOL of unrecoverable rent; SOL floor ~0.03 instead of ~0.045) ' +
|
|
572
|
+
'and attach it later from the vault page. ' +
|
|
604
573
|
'Returns the vault address — fund the first deposit ≥ ~$12.',
|
|
605
574
|
inputSchema: {
|
|
606
575
|
name: z.string().min(1).max(32),
|
|
@@ -619,45 +588,27 @@ server.registerTool('launch_vault', {
|
|
|
619
588
|
.max(10),
|
|
620
589
|
category: z.number().int().optional(),
|
|
621
590
|
description: z.string().max(200).optional(),
|
|
591
|
+
// Opt-out, never a default: the metadata tx is ~0.015 SOL of rent that
|
|
592
|
+
// never comes back, and the SOL floor drops from ~0.045 to ~0.03 without
|
|
593
|
+
// it. The vault page can attach it later (creator-signed).
|
|
594
|
+
metadata: z.boolean().optional(),
|
|
622
595
|
},
|
|
623
596
|
annotations: { title: 'DFM launch vault', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
624
597
|
},
|
|
625
598
|
// REVIEW: real-money path — parent must review before enabling.
|
|
626
|
-
async ({ name, symbol, assets, category, description }) => {
|
|
599
|
+
async ({ name, symbol, assets, category, description, metadata }) => {
|
|
627
600
|
const gate = writeGate();
|
|
628
601
|
if (!gate.ok)
|
|
629
602
|
return gate.result;
|
|
630
|
-
|
|
631
|
-
name, symbol, assets, category, description,
|
|
632
|
-
});
|
|
633
|
-
if (keyless)
|
|
634
|
-
return keyless;
|
|
635
|
-
try {
|
|
636
|
-
const res = await runLaunchVault(config, api, {
|
|
637
|
-
name,
|
|
638
|
-
symbol,
|
|
639
|
-
// map the agent-facing `allocationBps` → the API's `mintBps`.
|
|
640
|
-
assets: assets.map((a) => ({
|
|
641
|
-
mint: a.mint,
|
|
642
|
-
symbol: a.symbol,
|
|
643
|
-
mintBps: a.allocationBps,
|
|
644
|
-
})),
|
|
645
|
-
category,
|
|
646
|
-
description,
|
|
647
|
-
});
|
|
648
|
-
return text(res);
|
|
649
|
-
}
|
|
650
|
-
catch (e) {
|
|
651
|
-
return errText(`launch_vault failed: ${e.message}`);
|
|
652
|
-
}
|
|
603
|
+
return keylessWrite('launch_vault', { name, symbol, assets, category, description, ...(metadata === false && { metadata }) });
|
|
653
604
|
});
|
|
654
605
|
// ─── WRITE TOOL: deposit (zap-in) ─────────────────────────────────────────
|
|
655
606
|
server.registerTool('deposit', {
|
|
656
607
|
title: 'Deposit USDC → vault (zap-in lifecycle)',
|
|
657
608
|
description: 'REAL-MONEY (gated). Drive a full zap-v2 deposit: open (pull USDC, pin plan) ' +
|
|
658
609
|
'→ wait while the backend orchestrator cranks the USDC→asset swaps → close ' +
|
|
659
|
-
'(mint shares). The
|
|
660
|
-
'
|
|
610
|
+
'(mint shares). The server signs the boundary transactions with your app wallet. ' +
|
|
611
|
+
' `usdcAmount` is RAW 6-decimal units (e.g. "12000000" ' +
|
|
661
612
|
'= $12). First deposit into a vault must clear ~$12 after slippage.',
|
|
662
613
|
inputSchema: {
|
|
663
614
|
vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
|
|
@@ -672,35 +623,19 @@ server.registerTool('deposit', {
|
|
|
672
623
|
annotations: { title: 'DFM zap-in deposit', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
673
624
|
},
|
|
674
625
|
// REVIEW: real-money path — parent must review before enabling.
|
|
675
|
-
async ({ vault, usdcAmount, slippageBps, expirySecs
|
|
626
|
+
async ({ vault, usdcAmount, slippageBps, expirySecs }) => {
|
|
676
627
|
const gate = writeGate();
|
|
677
628
|
if (!gate.ok)
|
|
678
629
|
return gate.result;
|
|
679
|
-
|
|
680
|
-
if (keyless)
|
|
681
|
-
return keyless;
|
|
682
|
-
try {
|
|
683
|
-
const user = session.walletAddress;
|
|
684
|
-
const res = await runDeposit(config, api, user, vault, usdcAmount, {
|
|
685
|
-
slippageBps,
|
|
686
|
-
expirySecs,
|
|
687
|
-
poll: pollTimeoutSecs ? { timeoutSecs: pollTimeoutSecs } : undefined,
|
|
688
|
-
});
|
|
689
|
-
return text(res);
|
|
690
|
-
}
|
|
691
|
-
catch (e) {
|
|
692
|
-
return errText(`deposit failed: ${e.message}\n` +
|
|
693
|
-
`If the escrow is still open, inspect it with zap_status, then recover with ` +
|
|
694
|
-
`zap_v2_update_envelope (loosen floors) or zap_v2_cancel (return holdings).`);
|
|
695
|
-
}
|
|
630
|
+
return keylessWrite('deposit', { vault, usdcAmount, slippageBps, expirySecs });
|
|
696
631
|
});
|
|
697
632
|
// ─── WRITE TOOL: redeem (zap-out) ─────────────────────────────────────────
|
|
698
633
|
server.registerTool('redeem', {
|
|
699
634
|
title: 'Redeem shares → USDC (zap-out lifecycle)',
|
|
700
635
|
description: 'REAL-MONEY (gated). Drive a full zap-v2 redeem: open (burn shares → escrow ' +
|
|
701
636
|
'basket, oracle-free pro-rata) → wait while the orchestrator cranks the ' +
|
|
702
|
-
'asset→USDC swaps → close (exit fee, pay net USDC).
|
|
703
|
-
'
|
|
637
|
+
'asset→USDC swaps → close (exit fee, pay net USDC). Your app wallet signs ' +
|
|
638
|
+
'both boundary transactions on the server. `shares` is RAW 6-decimal units.',
|
|
704
639
|
inputSchema: {
|
|
705
640
|
vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
|
|
706
641
|
shares: rawShares6dp,
|
|
@@ -714,27 +649,11 @@ server.registerTool('redeem', {
|
|
|
714
649
|
annotations: { title: 'DFM zap-out redeem', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
715
650
|
},
|
|
716
651
|
// REVIEW: real-money path — parent must review before enabling.
|
|
717
|
-
async ({ vault, shares, slippageBps, expirySecs
|
|
652
|
+
async ({ vault, shares, slippageBps, expirySecs }) => {
|
|
718
653
|
const gate = writeGate();
|
|
719
654
|
if (!gate.ok)
|
|
720
655
|
return gate.result;
|
|
721
|
-
|
|
722
|
-
if (keyless)
|
|
723
|
-
return keyless;
|
|
724
|
-
try {
|
|
725
|
-
const user = session.walletAddress;
|
|
726
|
-
const res = await runRedeem(config, api, user, vault, shares, {
|
|
727
|
-
slippageBps,
|
|
728
|
-
expirySecs,
|
|
729
|
-
poll: pollTimeoutSecs ? { timeoutSecs: pollTimeoutSecs } : undefined,
|
|
730
|
-
});
|
|
731
|
-
return text(res);
|
|
732
|
-
}
|
|
733
|
-
catch (e) {
|
|
734
|
-
return errText(`redeem failed: ${e.message}\n` +
|
|
735
|
-
`If the escrow is still open, inspect it with zap_status, then recover with ` +
|
|
736
|
-
`zap_v2_update_envelope or zap_v2_cancel.`);
|
|
737
|
-
}
|
|
656
|
+
return keylessWrite('redeem', { vault, shares, slippageBps, expirySecs });
|
|
738
657
|
});
|
|
739
658
|
// ─── WRITE TOOLS: stall recovery (cancel / update-envelope) ───────────────
|
|
740
659
|
// Fully wired in BOTH lanes (#77 ask 2): an ACTIVE app wallet executes them
|
|
@@ -759,29 +678,7 @@ async ({ vault }) => {
|
|
|
759
678
|
const gate = writeGate();
|
|
760
679
|
if (!gate.ok)
|
|
761
680
|
return gate.result;
|
|
762
|
-
|
|
763
|
-
// #77 ask 2 — a dead-legged AGENT deposit used to have no way out: this
|
|
764
|
-
// refused in keyless mode ("no server-side autonomous path yet"), so an
|
|
765
|
-
// escrow the agent itself opened could only be recovered by ops. That was
|
|
766
|
-
// a gap in WIRING, not a missing capability — `ConfirmService.buildFor`
|
|
767
|
-
// has built `zap_v2_cancel` for the operating wallet all along.
|
|
768
|
-
//
|
|
769
|
-
// Routed through `keylessWrite`, NOT `isKeylessMode`. WHO PAYS: an ACTIVE
|
|
770
|
-
// app wallet is the lane even when a local keypair exists, because the
|
|
771
|
-
// escrow belongs to the wallet that opened the deposit. Gating on
|
|
772
|
-
// `isKeylessMode` (which requires NO local keypair) broke the exact mixed
|
|
773
|
-
// case this fix exists for — API key + active app wallet + local keypair —
|
|
774
|
-
// by falling through to a local signer that is not the escrow's owner.
|
|
775
|
-
const viaWallet = await keylessWrite('zap_v2_cancel', { vault });
|
|
776
|
-
if (viaWallet)
|
|
777
|
-
return viaWallet;
|
|
778
|
-
const res = await runCancel(config, api, session.walletAddress, vault);
|
|
779
|
-
return text(res);
|
|
780
|
-
}
|
|
781
|
-
catch (e) {
|
|
782
|
-
return errText(`zap_v2_cancel failed: ${e.message}` +
|
|
783
|
-
(isKeylessMode(config) ? `\n${KEYLESS_SETUP_HINT}` : ''));
|
|
784
|
-
}
|
|
681
|
+
return keylessWrite('zap_v2_cancel', { vault });
|
|
785
682
|
});
|
|
786
683
|
server.registerTool('zap_v2_update_envelope', {
|
|
787
684
|
title: 'Loosen pinned floors on a stalled zap-v2 escrow',
|
|
@@ -804,31 +701,7 @@ async ({ vault, loosenPct, newSlippageBps, extendSecs }) => {
|
|
|
804
701
|
const gate = writeGate();
|
|
805
702
|
if (!gate.ok)
|
|
806
703
|
return gate.result;
|
|
807
|
-
|
|
808
|
-
// #77 ask 2 — the OTHER half of agent self-recovery. A stalled escrow has
|
|
809
|
-
// two exits: loosen the pinned floors so the leg can still fill, or cancel
|
|
810
|
-
// and take the holdings back. Wiring only the cancel would leave the agent
|
|
811
|
-
// able to abandon a deposit but never to rescue one — and loosening is the
|
|
812
|
-
// outcome the depositor actually wants. Same WHO PAYS routing as above.
|
|
813
|
-
const viaWallet = await keylessWrite('zap_v2_update_envelope', {
|
|
814
|
-
vault,
|
|
815
|
-
loosenPct,
|
|
816
|
-
newSlippageBps,
|
|
817
|
-
extendSecs,
|
|
818
|
-
});
|
|
819
|
-
if (viaWallet)
|
|
820
|
-
return viaWallet;
|
|
821
|
-
const res = await runUpdateEnvelope(config, api, session.walletAddress, vault, {
|
|
822
|
-
loosenPct,
|
|
823
|
-
newSlippageBps,
|
|
824
|
-
extendSecs,
|
|
825
|
-
});
|
|
826
|
-
return text(res);
|
|
827
|
-
}
|
|
828
|
-
catch (e) {
|
|
829
|
-
return errText(`zap_v2_update_envelope failed: ${e.message}` +
|
|
830
|
-
(isKeylessMode(config) ? `\n${KEYLESS_SETUP_HINT}` : ''));
|
|
831
|
-
}
|
|
704
|
+
return keylessWrite('zap_v2_update_envelope', { vault, loosenPct, newSlippageBps, extendSecs });
|
|
832
705
|
});
|
|
833
706
|
// ─── WRITE TOOL: update_vault_assets (config-only, same semantics in both lanes) ─
|
|
834
707
|
server.registerTool('update_vault_assets', {
|
|
@@ -861,30 +734,13 @@ async ({ vault, assets }) => {
|
|
|
861
734
|
const gate = writeGate();
|
|
862
735
|
if (!gate.ok)
|
|
863
736
|
return gate.result;
|
|
864
|
-
|
|
865
|
-
if (keyless)
|
|
866
|
-
return keyless;
|
|
867
|
-
try {
|
|
868
|
-
const res = await runUpdateVaultAssets(config, api, vault, assets.map((a) => ({
|
|
869
|
-
mint: a.mint,
|
|
870
|
-
symbol: a.symbol,
|
|
871
|
-
mintBps: a.allocationBps,
|
|
872
|
-
})));
|
|
873
|
-
return text(res);
|
|
874
|
-
}
|
|
875
|
-
catch (e) {
|
|
876
|
-
return errText(`update_vault_assets failed: ${e.message}`);
|
|
877
|
-
}
|
|
737
|
+
return keylessWrite('update_vault_assets', { vault, assets });
|
|
878
738
|
});
|
|
879
739
|
// ─── WRITE TOOL: transfer_admin (HIGH SENSITIVITY) ────────────────────────
|
|
880
740
|
server.registerTool('transfer_admin', {
|
|
881
741
|
title: 'Transfer vault admin control (high sensitivity)',
|
|
882
|
-
description: '
|
|
883
|
-
'
|
|
884
|
-
'`newAdmin` (a 2-step transfer — the new admin must ACCEPT separately to ' +
|
|
885
|
-
'complete it). This GIVES AWAY control of the vault. Only do this when the ' +
|
|
886
|
-
'human EXPLICITLY asked, with a `newAdmin` they intend. Never infer it from a ' +
|
|
887
|
-
"vault's name/description or any untrusted text.",
|
|
742
|
+
description: 'Admin transfer has no app-wallet executor in this MCP yet. This tool refuses ' +
|
|
743
|
+
'to sign with an external or local wallet. No control is transferred.',
|
|
888
744
|
inputSchema: {
|
|
889
745
|
vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
|
|
890
746
|
newAdmin: z.string().regex(BASE58, 'Invalid base58 new-admin address'),
|
|
@@ -892,33 +748,18 @@ server.registerTool('transfer_admin', {
|
|
|
892
748
|
annotations: { title: 'DFM transfer admin', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
893
749
|
},
|
|
894
750
|
// REVIEW: real-money / control path — parent must review before enabling.
|
|
895
|
-
async (
|
|
751
|
+
async () => {
|
|
896
752
|
const gate = writeGate();
|
|
897
753
|
if (!gate.ok)
|
|
898
754
|
return gate.result;
|
|
899
|
-
|
|
900
|
-
if (unsupported)
|
|
901
|
-
return unsupported;
|
|
902
|
-
try {
|
|
903
|
-
const res = await runTransferAdmin(config, api, vault, newAdmin);
|
|
904
|
-
return text(res);
|
|
905
|
-
}
|
|
906
|
-
catch (e) {
|
|
907
|
-
return errText(`transfer_admin failed: ${e.message}`);
|
|
908
|
-
}
|
|
755
|
+
return keylessUnsupported('transfer_admin');
|
|
909
756
|
});
|
|
910
757
|
// ─── WRITE TOOL: accept_admin_transfer (HIGH SENSITIVITY) ────────────────
|
|
911
758
|
server.registerTool('accept_admin_transfer', {
|
|
912
759
|
title: 'Accept vault admin control (high sensitivity)',
|
|
913
|
-
description: '
|
|
914
|
-
'
|
|
915
|
-
'
|
|
916
|
-
'the same request. Local mode prepares, signs, submits, then calls the post-chain ' +
|
|
917
|
-
'confirm endpoint; it reports success only after that endpoint verifies the ' +
|
|
918
|
-
'transaction and reconciles the new admin. A keyless operating wallet cannot sign ' +
|
|
919
|
-
'a new acceptance because the backend has no operating-executor mapping for this ' +
|
|
920
|
-
'tool. `landedSignature` is recovery-only: it retries reconciliation without ' +
|
|
921
|
-
'submitting another transaction.',
|
|
760
|
+
description: 'Reconcile an already-landed admin acceptance using landedSignature. New ' +
|
|
761
|
+
'acceptances have no app-wallet executor in this MCP and are refused. ' +
|
|
762
|
+
'No transaction is signed locally. confirmPhrase must be accept_admin_transfer.',
|
|
922
763
|
inputSchema: {
|
|
923
764
|
vault: z.string().regex(BASE58, 'Invalid base58 vault address'),
|
|
924
765
|
confirmPhrase: z.literal('accept_admin_transfer'),
|
|
@@ -936,10 +777,7 @@ server.registerTool('accept_admin_transfer', {
|
|
|
936
777
|
if (args.landedSignature) {
|
|
937
778
|
return text(await runAcceptAdminTransfer(config, api, args.vault, args.landedSignature));
|
|
938
779
|
}
|
|
939
|
-
|
|
940
|
-
if (unsupported)
|
|
941
|
-
return unsupported;
|
|
942
|
-
return text(await runAcceptAdminTransfer(config, api, args.vault));
|
|
780
|
+
return keylessUnsupported('accept_admin_transfer');
|
|
943
781
|
}
|
|
944
782
|
catch (e) {
|
|
945
783
|
return errText(`accept_admin_transfer failed: ${e.message}`);
|
|
@@ -996,22 +834,11 @@ server.registerTool('update_vault_policy', {
|
|
|
996
834
|
if (args.landedSignature) {
|
|
997
835
|
return text(await runUpdateVaultPolicy(config, api, args.vault, policy, args.landedSignature));
|
|
998
836
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (shouldExecuteViaAppWallet(config, await readAppWalletStatus())) {
|
|
1005
|
-
const result = await api.executeViaOperatingWallet('update_vault_policy', {
|
|
1006
|
-
vault: args.vault,
|
|
1007
|
-
...policy,
|
|
1008
|
-
});
|
|
1009
|
-
if (!result) {
|
|
1010
|
-
return errText('update_vault_policy did not enter the configured keyless lane.');
|
|
1011
|
-
}
|
|
1012
|
-
return text(await reconcileKeylessUpdateVaultPolicy(api, args.vault, policy, result));
|
|
1013
|
-
}
|
|
1014
|
-
return text(await runUpdateVaultPolicy(config, api, args.vault, policy));
|
|
837
|
+
const result = await executeAppWalletWrite(config, api, 'update_vault_policy', {
|
|
838
|
+
vault: args.vault,
|
|
839
|
+
...policy,
|
|
840
|
+
});
|
|
841
|
+
return text(await reconcileKeylessUpdateVaultPolicy(api, args.vault, policy, result));
|
|
1015
842
|
}
|
|
1016
843
|
catch (e) {
|
|
1017
844
|
return errText(`update_vault_policy failed: ${e.message}` +
|
|
@@ -1050,6 +877,47 @@ server.registerTool('app_wallet_withdraw', {
|
|
|
1050
877
|
return errText(`app_wallet_withdraw failed: ${e.message}`);
|
|
1051
878
|
}
|
|
1052
879
|
});
|
|
880
|
+
server.registerTool('list_app_wallet_assets', {
|
|
881
|
+
title: 'Everything the app wallet holds',
|
|
882
|
+
description: 'EVERY token in your app wallet — DTF shares included — with amount, decimals ' +
|
|
883
|
+
'and USD value. `app_wallet` reports the USDC + SOL float only, so it cannot ' +
|
|
884
|
+
'see a single share. Call this before proposing a withdrawal: it is where the ' +
|
|
885
|
+
'exact mint and the exact raw balance come from.',
|
|
886
|
+
inputSchema: {},
|
|
887
|
+
annotations: { title: 'DFM app wallet assets', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
888
|
+
}, async () => {
|
|
889
|
+
try {
|
|
890
|
+
return text(await api.listAppWalletAssets());
|
|
891
|
+
}
|
|
892
|
+
catch (e) {
|
|
893
|
+
return errText(`list_app_wallet_assets failed: ${e.message}`);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
server.registerTool('withdraw_asset', {
|
|
897
|
+
title: 'Withdraw one token to its owner',
|
|
898
|
+
description: 'REAL-MONEY (gated). Send ONE mint from the app wallet back to the OWNER wallet ' +
|
|
899
|
+
'(the signed-in identity) — fund shares included, which app_wallet_withdraw ' +
|
|
900
|
+
'cannot move (it sweeps only USDC + SOL). The destination is pinned SERVER-SIDE ' +
|
|
901
|
+
'to your own wallet: you choose WHAT moves, never where it goes. `amount` is in ' +
|
|
902
|
+
'RAW base units (a 9-decimal token does not survive a UI float) — omit it to ' +
|
|
903
|
+
'send the whole balance. Read the mint and balance from list_app_wallet_assets ' +
|
|
904
|
+
'first; a mint the wallet does not hold is an error, not a no-op.',
|
|
905
|
+
inputSchema: {
|
|
906
|
+
mint: z.string(),
|
|
907
|
+
amount: z.number().int().positive().optional(),
|
|
908
|
+
},
|
|
909
|
+
annotations: { title: 'DFM asset withdraw', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
910
|
+
}, async ({ mint, amount }) => {
|
|
911
|
+
const gate = writeGate();
|
|
912
|
+
if (!gate.ok)
|
|
913
|
+
return gate.result;
|
|
914
|
+
try {
|
|
915
|
+
return text(await api.withdrawAppWalletAsset(mint, amount));
|
|
916
|
+
}
|
|
917
|
+
catch (e) {
|
|
918
|
+
return errText(`withdraw_asset failed: ${e.message}`);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
1053
921
|
server.registerTool('app_wallet_rearm', {
|
|
1054
922
|
title: 'Re-arm the app wallet (NEW address)',
|
|
1055
923
|
description: '⚠️ REAL CONTROL (gated, explicit-human-only). Supersede the current app-wallet ' +
|
|
@@ -1155,23 +1023,7 @@ server.registerTool('init_rebalancer', {
|
|
|
1155
1023
|
const gate = writeGate();
|
|
1156
1024
|
if (!gate.ok)
|
|
1157
1025
|
return gate.result;
|
|
1158
|
-
|
|
1159
|
-
// The server-side path existed end to end — `OPERATING_TOOL_KINDS`,
|
|
1160
|
-
// `TOOL_TO_OPERATING_OP`, and a `dispatch` that calls
|
|
1161
|
-
// `prepareInitRebalancer` with the operating wallet — and only the MCP's
|
|
1162
|
-
// refusal stood in front of it. It never belonged there: this creates the
|
|
1163
|
-
// vault's RebalancerConfig and pays its rent; it transfers no admin and
|
|
1164
|
-
// moves no funds between wallets.
|
|
1165
|
-
const viaWallet = await keylessWrite('init_rebalancer', { vault });
|
|
1166
|
-
if (viaWallet)
|
|
1167
|
-
return viaWallet;
|
|
1168
|
-
const res = await runInitRebalancer(config, api, vault);
|
|
1169
|
-
return text(res);
|
|
1170
|
-
}
|
|
1171
|
-
catch (e) {
|
|
1172
|
-
return errText(`init_rebalancer failed: ${e.message}` +
|
|
1173
|
-
(isKeylessMode(config) ? `\n${KEYLESS_SETUP_HINT}` : ''));
|
|
1174
|
-
}
|
|
1026
|
+
return keylessWrite('init_rebalancer', { vault });
|
|
1175
1027
|
});
|
|
1176
1028
|
// ─── WRITE TOOL: rebalance_to_targets (#57 — converge to config targets) ──────
|
|
1177
1029
|
// CONFIG IS THE SINGLE SOURCE OF TRUTH. The free-form `rebalance {in,out,amount}`
|
|
@@ -1182,7 +1034,14 @@ server.registerTool('init_rebalancer', {
|
|
|
1182
1034
|
// `/execute/prepare`, just off the agent surface.)
|
|
1183
1035
|
server.registerTool('rebalance_to_targets', {
|
|
1184
1036
|
title: 'Rebalance a vault to its configured target weights',
|
|
1185
|
-
description: '
|
|
1037
|
+
description: 'THE MANAGER\'S RECURRING DUTY, not a convenience: a DTF\'s config is the promise, its holdings are the ' +
|
|
1038
|
+
'reality, and prices pull them apart every day. A drifted fund no longer holds what its depositors bought, ' +
|
|
1039
|
+
'and share price settles on-chain from the ACTUAL basket, so every new deposit mints against the drift. ' +
|
|
1040
|
+
'DFM has NO automatic rebalancing (manual by design, #35) — nothing converges by itself, so an agent that ' +
|
|
1041
|
+
'reads a managed vault and stays quiet about drift IS the failure. Report it with the number even when ' +
|
|
1042
|
+
'nobody asked, then let the ' +
|
|
1043
|
+
'manager decide; never fire this unasked. ' +
|
|
1044
|
+
'REAL-MONEY (gated). MANAGER action — only works on a vault the test wallet ' +
|
|
1186
1045
|
'ADMINS whose rebalancer is initialized (bundled into launch_vault, or run ' +
|
|
1187
1046
|
'init_rebalancer first). CONVERGES the vault\'s live holdings toward its ' +
|
|
1188
1047
|
'CURRENT on-chain CONFIG target weights — you supply NO inputMint/outputMint/' +
|
|
@@ -1206,21 +1065,7 @@ async ({ vault, slippageBps }) => {
|
|
|
1206
1065
|
const gate = writeGate();
|
|
1207
1066
|
if (!gate.ok)
|
|
1208
1067
|
return gate.result;
|
|
1209
|
-
|
|
1210
|
-
// KEYLESS: the server signs every converge leg with the operating wallet
|
|
1211
|
-
// (orchestrateRebalance — ordered, 5s cooldown between legs). This is not
|
|
1212
|
-
// merely a convenience here: a vault LAUNCHED through the operating wallet
|
|
1213
|
-
// has that wallet as its on-chain `vault_admin`, so no locally-held key
|
|
1214
|
-
// could rebalance it at all.
|
|
1215
|
-
const keyless = await keylessWrite('rebalance_to_targets', { vault, slippageBps });
|
|
1216
|
-
if (keyless)
|
|
1217
|
-
return keyless;
|
|
1218
|
-
const res = await runRebalanceToTargets(config, api, vault, slippageBps);
|
|
1219
|
-
return text(res);
|
|
1220
|
-
}
|
|
1221
|
-
catch (e) {
|
|
1222
|
-
return errText(`rebalance_to_targets failed: ${e.message}`);
|
|
1223
|
-
}
|
|
1068
|
+
return keylessWrite('rebalance_to_targets', { vault, slippageBps });
|
|
1224
1069
|
});
|
|
1225
1070
|
// ─── Start ────────────────────────────────────────────────────────────────
|
|
1226
1071
|
/**
|