@elisym/mcp 0.13.0 → 0.15.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/index.js +562 -127
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { LIMITS, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, RELAYS, toDTag, JobWaitTimeoutError, formatAssetAmount, estimateNetworkBaseline, formatSol as formatSol$1, USDC_SOLANA_DEVNET, estimateSolFeeLamports, formatFeeBreakdown, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, parseAssetAmount, ElisymIdentity, ElisymClient, NATIVE_SOL, resolveKnownAsset, formatNetworkBaseline, getProtocolProgramId, getProtocolConfig, assetKey, assetByKey, KNOWN_ASSETS } from '@elisym/sdk';
|
|
2
|
+
import { LIMITS, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, RELAYS, toDTag, JobWaitTimeoutError, decodeJobPayload, utf8ByteLength, formatAssetAmount, estimateNetworkBaseline, formatSol as formatSol$1, USDC_SOLANA_DEVNET, estimateSolFeeLamports, formatFeeBreakdown, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, parseAssetAmount, ElisymIdentity, ElisymClient, NATIVE_SOL, resolveKnownAsset, formatNetworkBaseline, getProtocolProgramId, getProtocolConfig, assetKey, assetByKey, KNOWN_ASSETS } from '@elisym/sdk';
|
|
3
3
|
import { listAgents, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, resolveAgent, loadAgent, globalConfigPath, writeYaml, writeFileAtomic as writeFileAtomic$1 } from '@elisym/sdk/agent-store';
|
|
4
|
-
import { loadGlobalConfig, writeGlobalConfig } from '@elisym/sdk/node';
|
|
4
|
+
import { createIrohTransport, loadGlobalConfig, writeGlobalConfig } from '@elisym/sdk/node';
|
|
5
5
|
import { getBase58Encoder, getBase58Decoder, generateKeyPairSigner, createSolanaRpc, address, createSolanaRpcSubscriptions, sendAndConfirmTransactionFactory, getSignatureFromTransaction, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, createKeyPairSignerFromBytes, isAddress } from '@solana/kit';
|
|
6
6
|
import { Command } from 'commander';
|
|
7
7
|
import { generateSecretKey, nip19, getPublicKey } from 'nostr-tools';
|
|
8
|
-
import {
|
|
9
|
-
import { homedir, platform } from 'node:os';
|
|
10
|
-
import { dirname, join, isAbsolute,
|
|
11
|
-
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { realpath, readFile, stat, rm, writeFile, rename, unlink } from 'node:fs/promises';
|
|
9
|
+
import { tmpdir, homedir, platform } from 'node:os';
|
|
10
|
+
import { dirname, join, resolve, isAbsolute, basename, relative } from 'node:path';
|
|
11
|
+
import { readFileSync, mkdtempSync } from 'node:fs';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
14
14
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -346,6 +346,9 @@ function checkLen(field, value, max) {
|
|
|
346
346
|
var MAX_INPUT_LEN = LIMITS.MAX_INPUT_LENGTH;
|
|
347
347
|
var MAX_CAPABILITIES = LIMITS.MAX_CAPABILITIES;
|
|
348
348
|
var MAX_TIMEOUT_SECS = LIMITS.MAX_TIMEOUT_SECS;
|
|
349
|
+
LIMITS.NIP44_MAX_PLAINTEXT_BYTES;
|
|
350
|
+
LIMITS.MAX_ENCRYPTED_INLINE_BYTES;
|
|
351
|
+
LIMITS.MAX_REINLINE_TEXT_BYTES;
|
|
349
352
|
var MAX_NPUB_LEN = 128;
|
|
350
353
|
var MAX_EVENT_ID_LEN = 128;
|
|
351
354
|
var MAX_PAYMENT_REQ_LEN = 1e4;
|
|
@@ -684,6 +687,32 @@ async function installToConfig(path, entry, agentRebind) {
|
|
|
684
687
|
await safeRewriteJson(path, raw, config);
|
|
685
688
|
return "installed";
|
|
686
689
|
}
|
|
690
|
+
function ensureIrohTransport(agent) {
|
|
691
|
+
if (agent.irohTransport) {
|
|
692
|
+
return agent.irohTransport;
|
|
693
|
+
}
|
|
694
|
+
let storePath;
|
|
695
|
+
if (agent.agentDir !== void 0) {
|
|
696
|
+
storePath = join(agent.agentDir, ".iroh");
|
|
697
|
+
} else {
|
|
698
|
+
storePath = mkdtempSync(join(tmpdir(), "elisym-iroh-"));
|
|
699
|
+
agent.irohStoreDir = storePath;
|
|
700
|
+
}
|
|
701
|
+
agent.irohTransport = createIrohTransport({ storePath });
|
|
702
|
+
return agent.irohTransport;
|
|
703
|
+
}
|
|
704
|
+
async function shutdownIrohTransport(agent) {
|
|
705
|
+
if (agent.irohTransport) {
|
|
706
|
+
await agent.irohTransport.shutdown().catch(() => {
|
|
707
|
+
});
|
|
708
|
+
agent.irohTransport = void 0;
|
|
709
|
+
}
|
|
710
|
+
if (agent.irohStoreDir !== void 0) {
|
|
711
|
+
await rm(agent.irohStoreDir, { recursive: true, force: true }).catch(() => {
|
|
712
|
+
});
|
|
713
|
+
agent.irohStoreDir = void 0;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
687
716
|
function createLogger(destination) {
|
|
688
717
|
const opts = {
|
|
689
718
|
name: "elisym-mcp",
|
|
@@ -725,7 +754,7 @@ async function buildEffectiveLimits() {
|
|
|
725
754
|
throw new Error(`Duplicate session_spend_limit entry in ${globalConfigPath()}: ${key}`);
|
|
726
755
|
}
|
|
727
756
|
seen.add(key);
|
|
728
|
-
map.set(key, parseAssetAmount(asset, entry.amount
|
|
757
|
+
map.set(key, parseAssetAmount(asset, entry.amount));
|
|
729
758
|
}
|
|
730
759
|
return map;
|
|
731
760
|
}
|
|
@@ -817,6 +846,14 @@ async function buildAgentInstance(name, config) {
|
|
|
817
846
|
agentDir: resolved?.dir
|
|
818
847
|
};
|
|
819
848
|
}
|
|
849
|
+
function scrubAgent(ctx, agent) {
|
|
850
|
+
agent.client.close();
|
|
851
|
+
if (agent.solanaKeypair) {
|
|
852
|
+
agent.solanaKeypair.secretKey.fill(0);
|
|
853
|
+
}
|
|
854
|
+
agent.identity.scrub();
|
|
855
|
+
ctx.registry.delete(agent.name);
|
|
856
|
+
}
|
|
820
857
|
var agentTools = [
|
|
821
858
|
defineTool({
|
|
822
859
|
name: "create_agent",
|
|
@@ -905,34 +942,34 @@ Solana: ${solanaSigner.address}
|
|
|
905
942
|
}
|
|
906
943
|
} catch {
|
|
907
944
|
}
|
|
945
|
+
let old;
|
|
908
946
|
try {
|
|
909
|
-
|
|
910
|
-
if (old.name !== input.name) {
|
|
911
|
-
old.client.close();
|
|
912
|
-
if (old.solanaKeypair) {
|
|
913
|
-
old.solanaKeypair.secretKey.fill(0);
|
|
914
|
-
}
|
|
915
|
-
old.identity.scrub();
|
|
916
|
-
ctx.registry.delete(old.name);
|
|
917
|
-
}
|
|
947
|
+
old = ctx.active();
|
|
918
948
|
} catch {
|
|
919
949
|
}
|
|
920
950
|
if (ctx.registry.has(input.name)) {
|
|
951
|
+
if (old && old.name !== input.name) {
|
|
952
|
+
scrubAgent(ctx, old);
|
|
953
|
+
}
|
|
921
954
|
ctx.activeAgentName = input.name;
|
|
922
955
|
const agent = ctx.active();
|
|
923
|
-
const
|
|
924
|
-
return textResult(`Switched to agent "${input.name}" (${
|
|
956
|
+
const npub2 = agent.identity.npub;
|
|
957
|
+
return textResult(`Switched to agent "${input.name}" (${npub2}).`);
|
|
925
958
|
}
|
|
959
|
+
let instance;
|
|
926
960
|
try {
|
|
927
961
|
const config = await loadAgentConfig(input.name);
|
|
928
|
-
|
|
929
|
-
ctx.register(instance, true);
|
|
930
|
-
const npub = instance.identity.npub;
|
|
931
|
-
return textResult(`Loaded and switched to agent "${input.name}" (${npub}).`);
|
|
962
|
+
instance = await buildAgentInstance(input.name, config);
|
|
932
963
|
} catch (e) {
|
|
933
964
|
const msg = e instanceof Error ? e.message : String(e);
|
|
934
965
|
return errorResult(`Failed to load agent "${input.name}": ${msg}`);
|
|
935
966
|
}
|
|
967
|
+
if (old && old.name !== input.name) {
|
|
968
|
+
scrubAgent(ctx, old);
|
|
969
|
+
}
|
|
970
|
+
ctx.register(instance, true);
|
|
971
|
+
const npub = instance.identity.npub;
|
|
972
|
+
return textResult(`Loaded and switched to agent "${input.name}" (${npub}).`);
|
|
936
973
|
}
|
|
937
974
|
}),
|
|
938
975
|
defineTool({
|
|
@@ -994,25 +1031,124 @@ Solana: ${solanaSigner.address}
|
|
|
994
1031
|
if (!agent) {
|
|
995
1032
|
return errorResult(`Agent "${input.name}" is not loaded.`);
|
|
996
1033
|
}
|
|
997
|
-
agent
|
|
998
|
-
if (agent.solanaKeypair) {
|
|
999
|
-
agent.solanaKeypair.secretKey.fill(0);
|
|
1000
|
-
}
|
|
1001
|
-
agent.identity.scrub();
|
|
1002
|
-
ctx.registry.delete(input.name);
|
|
1034
|
+
scrubAgent(ctx, agent);
|
|
1003
1035
|
return textResult(`Agent "${input.name}" stopped and removed.`);
|
|
1004
1036
|
}
|
|
1005
1037
|
})
|
|
1006
1038
|
];
|
|
1007
1039
|
var execFileP = promisify(execFile);
|
|
1008
1040
|
var MAX_INPUT_PATH_LEN = 4096;
|
|
1041
|
+
var SENSITIVE_NAME_RE = /(^|[/\\])(\.secrets\.json|\.env(\..+)?|id_rsa|id_dsa|id_ecdsa|id_ed25519|.*-keypair\.json|.*\.pem|.*\.key|\.bashrc|\.bash_profile|\.bash_login|\.bash_logout|\.bash_aliases|\.profile|\.zshrc|\.zprofile|\.zshenv|\.zlogin|\.zlogout|config\.fish|\.gitconfig|\.npmrc|\.netrc|crontab|sudoers|bash\.bashrc|.*\.service|.*\.desktop)$/i;
|
|
1042
|
+
var SENSITIVE_DIR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
1043
|
+
".elisym",
|
|
1044
|
+
".ssh",
|
|
1045
|
+
".aws",
|
|
1046
|
+
".gnupg",
|
|
1047
|
+
".git",
|
|
1048
|
+
"launchagents",
|
|
1049
|
+
"launchdaemons",
|
|
1050
|
+
"autostart",
|
|
1051
|
+
"systemd",
|
|
1052
|
+
"sudoers.d",
|
|
1053
|
+
"cron.d",
|
|
1054
|
+
"cron.daily",
|
|
1055
|
+
"cron.hourly",
|
|
1056
|
+
"cron.weekly",
|
|
1057
|
+
"cron.monthly",
|
|
1058
|
+
"crontabs",
|
|
1059
|
+
"profile.d",
|
|
1060
|
+
"init.d"
|
|
1061
|
+
]);
|
|
1062
|
+
function isSensitiveInputPath(absPath) {
|
|
1063
|
+
if (SENSITIVE_NAME_RE.test(absPath)) {
|
|
1064
|
+
return true;
|
|
1065
|
+
}
|
|
1066
|
+
if (absPath === "/proc" || absPath.startsWith("/proc/")) {
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
const segments = absPath.split(/[/\\]+/);
|
|
1070
|
+
return segments.some((segment) => SENSITIVE_DIR_SEGMENTS.has(segment.toLowerCase()));
|
|
1071
|
+
}
|
|
1072
|
+
async function resolveOutputPath(outputPath, options) {
|
|
1073
|
+
if (outputPath.length > MAX_INPUT_PATH_LEN) {
|
|
1074
|
+
throw new Error(
|
|
1075
|
+
`output_path too long: ${outputPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const cwd = resolve(process.cwd());
|
|
1079
|
+
const logicalPath = isAbsolute(outputPath) ? resolve(outputPath) : resolve(cwd, outputPath);
|
|
1080
|
+
const realParent = await realpath(dirname(logicalPath)).catch(() => dirname(logicalPath));
|
|
1081
|
+
const absPath = resolve(realParent, basename(logicalPath));
|
|
1082
|
+
const realDest = await realpath(logicalPath).catch(() => void 0);
|
|
1083
|
+
const writeTarget = realDest ?? absPath;
|
|
1084
|
+
const sensitiveCandidates = realDest !== void 0 ? [absPath, logicalPath, realDest] : [absPath, logicalPath];
|
|
1085
|
+
if (sensitiveCandidates.some((candidate) => isSensitiveInputPath(candidate))) {
|
|
1086
|
+
throw new Error(
|
|
1087
|
+
`Refusing to write a job result to a sensitive path: ${writeTarget}. Choose a destination outside secret/config/auto-run locations.`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (!options?.allowOutsideCwd) {
|
|
1091
|
+
const realCwd = await realpath(cwd).catch(() => cwd);
|
|
1092
|
+
const rel = relative(realCwd, writeTarget);
|
|
1093
|
+
const insideCwd = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1094
|
+
if (!insideCwd) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`output_path "${writeTarget}" resolves outside the working directory (${realCwd}). Choose a destination under the working directory or pass allow_outside_cwd: true.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return writeTarget;
|
|
1101
|
+
}
|
|
1009
1102
|
var GIT_TIMEOUT_MS = 3e4;
|
|
1010
|
-
var GIT_MAX_BUFFER =
|
|
1011
|
-
|
|
1103
|
+
var GIT_MAX_BUFFER = LIMITS.MAX_REINLINE_TEXT_BYTES + MAX_INPUT_LEN;
|
|
1104
|
+
var GIT_SAFETY_ARGS = [
|
|
1105
|
+
"-c",
|
|
1106
|
+
"core.fsmonitor=",
|
|
1107
|
+
"-c",
|
|
1108
|
+
"diff.external=",
|
|
1109
|
+
"-c",
|
|
1110
|
+
"core.hooksPath=/dev/null"
|
|
1111
|
+
];
|
|
1112
|
+
function isValidGitRef(ref) {
|
|
1113
|
+
if (ref.length === 0 || ref.length > 256) {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
if (ref.startsWith("-") || ref.includes("..")) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
return /^[A-Za-z0-9._/@~^-]+$/.test(ref);
|
|
1120
|
+
}
|
|
1121
|
+
async function validateInputPath(inputPath, options) {
|
|
1012
1122
|
if (inputPath.length > MAX_INPUT_PATH_LEN) {
|
|
1013
1123
|
throw new Error(`input_path too long: ${inputPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`);
|
|
1014
1124
|
}
|
|
1015
|
-
const
|
|
1125
|
+
const cwd = resolve(process.cwd());
|
|
1126
|
+
const logicalPath = isAbsolute(inputPath) ? resolve(inputPath) : resolve(cwd, inputPath);
|
|
1127
|
+
let absPath;
|
|
1128
|
+
try {
|
|
1129
|
+
absPath = await realpath(logicalPath);
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
const code = e.code;
|
|
1132
|
+
if (code === "ENOENT") {
|
|
1133
|
+
throw new Error(`input_path does not exist: ${logicalPath}`);
|
|
1134
|
+
}
|
|
1135
|
+
throw new Error(`Cannot resolve input_path "${logicalPath}": ${e.message}`);
|
|
1136
|
+
}
|
|
1137
|
+
if (isSensitiveInputPath(absPath) || isSensitiveInputPath(logicalPath)) {
|
|
1138
|
+
throw new Error(
|
|
1139
|
+
`Refusing to read a sensitive file as job input: ${absPath}. Secret keys, .env, SSH/keypair files, ~/.elisym and /proc are blocked.`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
if (!options?.allowOutsideCwd) {
|
|
1143
|
+
const realCwd = await realpath(cwd).catch(() => cwd);
|
|
1144
|
+
const rel = relative(realCwd, absPath);
|
|
1145
|
+
const insideCwd = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1146
|
+
if (!insideCwd) {
|
|
1147
|
+
throw new Error(
|
|
1148
|
+
`input_path "${absPath}" resolves outside the working directory (${realCwd}). Move the file under the working directory or pass allow_outside_cwd: true.`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1016
1152
|
let stats;
|
|
1017
1153
|
try {
|
|
1018
1154
|
stats = await stat(absPath);
|
|
@@ -1026,26 +1162,44 @@ async function readJobInputFile(inputPath) {
|
|
|
1026
1162
|
if (!stats.isFile()) {
|
|
1027
1163
|
throw new Error(`input_path is not a regular file: ${absPath}`);
|
|
1028
1164
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1165
|
+
return { absPath, size: stats.size };
|
|
1166
|
+
}
|
|
1167
|
+
async function isProbablyText(absPath, size) {
|
|
1168
|
+
if (size > LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
const bytes = await readFile(absPath);
|
|
1172
|
+
if (bytes.includes(0)) {
|
|
1173
|
+
return false;
|
|
1033
1174
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1175
|
+
try {
|
|
1176
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1177
|
+
decoder.decode(bytes);
|
|
1178
|
+
return true;
|
|
1179
|
+
} catch {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async function prepareFileInput(inputPath, options) {
|
|
1184
|
+
const { absPath, size } = await validateInputPath(inputPath, options);
|
|
1185
|
+
if (size === 0) {
|
|
1186
|
+
throw new Error("input_path is an empty file - nothing to send.");
|
|
1187
|
+
}
|
|
1188
|
+
if (size > LIMITS.MAX_FILE_SIZE) {
|
|
1036
1189
|
throw new Error(
|
|
1037
|
-
`input_path
|
|
1190
|
+
`input_path too large: ${size} bytes (max ${LIMITS.MAX_FILE_SIZE} for a file transfer).`
|
|
1038
1191
|
);
|
|
1039
1192
|
}
|
|
1040
|
-
|
|
1193
|
+
const mime = await isProbablyText(absPath, size) ? "text/plain" : "application/octet-stream";
|
|
1194
|
+
return { absPath, size, name: basename(absPath), mime };
|
|
1041
1195
|
}
|
|
1042
1196
|
async function execGit(repoPath, args) {
|
|
1043
1197
|
try {
|
|
1044
|
-
const { stdout } = await execFileP("git", args, {
|
|
1198
|
+
const { stdout } = await execFileP("git", [...GIT_SAFETY_ARGS, ...args], {
|
|
1045
1199
|
cwd: repoPath,
|
|
1046
1200
|
timeout: GIT_TIMEOUT_MS,
|
|
1047
1201
|
maxBuffer: GIT_MAX_BUFFER,
|
|
1048
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
1202
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1" }
|
|
1049
1203
|
});
|
|
1050
1204
|
return stdout;
|
|
1051
1205
|
} catch (e) {
|
|
@@ -1099,18 +1253,23 @@ async function computeGitDiff(repoPath, base) {
|
|
|
1099
1253
|
let args;
|
|
1100
1254
|
let describedRange;
|
|
1101
1255
|
if (base) {
|
|
1102
|
-
|
|
1256
|
+
if (!isValidGitRef(base)) {
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Invalid "base": ${base}. Use a branch/tag/commit ref (letters, digits, ". _ / @ ~ ^ -", no leading "-", no "..").`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "--end-of-options", `${base}...HEAD`];
|
|
1103
1262
|
describedRange = `${base}...HEAD`;
|
|
1104
1263
|
} else if (await isDirty(absRepo)) {
|
|
1105
|
-
args = ["diff", "HEAD"];
|
|
1264
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "HEAD"];
|
|
1106
1265
|
describedRange = "HEAD (working tree, uncommitted changes)";
|
|
1107
1266
|
} else {
|
|
1108
1267
|
const detected = await detectDefaultBase(absRepo);
|
|
1109
1268
|
if (detected) {
|
|
1110
|
-
args = ["diff", `${detected}...HEAD`];
|
|
1269
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "--end-of-options", `${detected}...HEAD`];
|
|
1111
1270
|
describedRange = `${detected}...HEAD`;
|
|
1112
1271
|
} else {
|
|
1113
|
-
args = ["diff", "HEAD"];
|
|
1272
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "HEAD"];
|
|
1114
1273
|
describedRange = "HEAD (no main/master detected)";
|
|
1115
1274
|
}
|
|
1116
1275
|
}
|
|
@@ -1120,9 +1279,10 @@ async function computeGitDiff(repoPath, base) {
|
|
|
1120
1279
|
`No changes in range ${describedRange}. Nothing to review - commit work, pass an explicit "base", or check the repo path.`
|
|
1121
1280
|
);
|
|
1122
1281
|
}
|
|
1123
|
-
|
|
1282
|
+
const diffBytes = utf8ByteLength(diff);
|
|
1283
|
+
if (diffBytes > LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
1124
1284
|
throw new Error(
|
|
1125
|
-
`Diff for range ${describedRange} is ${
|
|
1285
|
+
`Diff for range ${describedRange} is ${diffBytes} bytes (max ${LIMITS.MAX_REINLINE_TEXT_BYTES}). Pass a narrower "base" or split the review.`
|
|
1126
1286
|
);
|
|
1127
1287
|
}
|
|
1128
1288
|
return { diff, describedRange };
|
|
@@ -1277,8 +1437,11 @@ var INJECTION_PATTERNS = [
|
|
|
1277
1437
|
var STRICT_INJECTION_PATTERNS = INJECTION_PATTERNS.filter((p) => !p.noisy);
|
|
1278
1438
|
function stripDangerousUnicode(text) {
|
|
1279
1439
|
return text.replace(
|
|
1440
|
+
// C0 (minus \n,\t) + C1 controls; bidi marks/overrides/isolates (061C, 200E-200F,
|
|
1441
|
+
// 202A-202E, 2066-2069); zero-width + word-joiner + invisible-operator format chars
|
|
1442
|
+
// (200B-200D, 2060-2064, 180E, FEFF); tag chars; replacement char.
|
|
1280
1443
|
// eslint-disable-next-line no-control-regex
|
|
1281
|
-
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\u202A-\u202E\u2066-\u2069\
|
|
1444
|
+
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\u061C\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF\uFFFD]|[\uDB40][\uDC01-\uDC7F]/g,
|
|
1282
1445
|
""
|
|
1283
1446
|
);
|
|
1284
1447
|
}
|
|
@@ -1294,9 +1457,15 @@ function detectInjections(text, includeNoisy) {
|
|
|
1294
1457
|
if (normalized.length <= INJECTION_SCAN_BUDGET) {
|
|
1295
1458
|
return patterns.some((p) => p.pattern.test(normalized));
|
|
1296
1459
|
}
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1460
|
+
const OVERLAP = 256;
|
|
1461
|
+
const step = INJECTION_SCAN_BUDGET - OVERLAP;
|
|
1462
|
+
for (let start = 0; start < normalized.length; start += step) {
|
|
1463
|
+
const window = normalized.slice(start, start + INJECTION_SCAN_BUDGET);
|
|
1464
|
+
if (patterns.some((p) => p.pattern.test(window))) {
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return false;
|
|
1300
1469
|
}
|
|
1301
1470
|
function scanForInjections(text, mode = "full") {
|
|
1302
1471
|
return detectInjections(text, mode === "full");
|
|
@@ -1305,8 +1474,11 @@ function isLikelyBase64(s) {
|
|
|
1305
1474
|
if (s.length < 64) {
|
|
1306
1475
|
return false;
|
|
1307
1476
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1477
|
+
if (/\s/.test(s)) {
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
const nonBase64Chars = s.replace(/[A-Za-z0-9+/=]/g, "");
|
|
1481
|
+
return nonBase64Chars.length / s.length < 0.02;
|
|
1310
1482
|
}
|
|
1311
1483
|
function sanitizeUntrusted(input, kind = "text", options) {
|
|
1312
1484
|
let text = stripDangerousUnicode(input);
|
|
@@ -1352,7 +1524,13 @@ var CustomerJobEntrySchema = z.object({
|
|
|
1352
1524
|
completedAt: z.number().int().nonnegative(),
|
|
1353
1525
|
resultPreview: z.string().max(RESULT_PREVIEW_MAX_LEN).optional(),
|
|
1354
1526
|
paymentSig: z.string().max(128).optional(),
|
|
1355
|
-
customerFeedback: FeedbackSchema.optional()
|
|
1527
|
+
customerFeedback: FeedbackSchema.optional(),
|
|
1528
|
+
/** JSON-serialized FileAttachment when the result is a file (fetched via fetch_job_file). */
|
|
1529
|
+
attachmentJson: z.string().max(8192).optional(),
|
|
1530
|
+
/** Local path the result file was downloaded to (set by fetch_job_file). */
|
|
1531
|
+
resultFilePath: z.string().max(4096).optional(),
|
|
1532
|
+
/** Unix ms when the result file was downloaded. */
|
|
1533
|
+
fetchedAt: z.number().int().nonnegative().optional()
|
|
1356
1534
|
}).strict();
|
|
1357
1535
|
var CustomerHistorySchema = z.object({
|
|
1358
1536
|
version: z.literal(1),
|
|
@@ -1363,14 +1541,12 @@ var writeLocks = /* @__PURE__ */ new Map();
|
|
|
1363
1541
|
function withLock(path, fn) {
|
|
1364
1542
|
const previous = writeLocks.get(path) ?? Promise.resolve();
|
|
1365
1543
|
const next = previous.then(fn, fn);
|
|
1366
|
-
|
|
1367
|
-
path
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
})
|
|
1373
|
-
);
|
|
1544
|
+
const wrapped = next.finally(() => {
|
|
1545
|
+
if (writeLocks.get(path) === wrapped) {
|
|
1546
|
+
writeLocks.delete(path);
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
writeLocks.set(path, wrapped);
|
|
1374
1550
|
return next;
|
|
1375
1551
|
}
|
|
1376
1552
|
function pathFor(agentDir) {
|
|
@@ -1453,6 +1629,16 @@ var GetJobResultSchema = z.object({
|
|
|
1453
1629
|
timeout_secs: z.number().int().min(1).max(600).default(60),
|
|
1454
1630
|
lookback_secs: z.number().int().min(60).max(7 * 24 * 3600).default(24 * 3600).describe("How far back to search for the result. Defaults to 24h.")
|
|
1455
1631
|
});
|
|
1632
|
+
var FetchJobFileSchema = z.object({
|
|
1633
|
+
job_event_id: z.string(),
|
|
1634
|
+
output_path: z.string().min(1).max(4096).describe("Local path to write the downloaded result file to."),
|
|
1635
|
+
allow_outside_cwd: z.boolean().default(false).describe(
|
|
1636
|
+
"Allow writing outside the MCP server working directory. Off by default: the bytes come from an untrusted provider, so writes are confined to the working directory subtree (and never to a secret/auto-run path) unless this is set."
|
|
1637
|
+
),
|
|
1638
|
+
provider_npub: z.string().optional(),
|
|
1639
|
+
kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
|
|
1640
|
+
timeout_secs: z.number().int().min(1).max(600).default(300)
|
|
1641
|
+
});
|
|
1456
1642
|
var ListMyJobsSchema = z.object({
|
|
1457
1643
|
limit: z.number().int().min(1).max(50).default(20),
|
|
1458
1644
|
kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
|
|
@@ -1483,7 +1669,10 @@ var SubmitAndPayJobFromFileSchema = z.object({
|
|
|
1483
1669
|
capability: z.string().min(1).max(64).default("general"),
|
|
1484
1670
|
kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
|
|
1485
1671
|
timeout_secs: z.number().int().min(1).max(600).default(300),
|
|
1486
|
-
max_price_lamports: z.number().int().optional()
|
|
1672
|
+
max_price_lamports: z.number().int().optional(),
|
|
1673
|
+
allow_outside_cwd: z.boolean().default(false).describe(
|
|
1674
|
+
"Allow reading a file outside the MCP server working directory. Off by default - the file content is forwarded to the provider before payment and is invisible in the transcript, so reads are confined to the working dir unless this is set. Sensitive files (secret keys, .env, SSH/keypair, ~/.elisym, /proc) are always refused."
|
|
1675
|
+
)
|
|
1487
1676
|
});
|
|
1488
1677
|
var SubmitDiffReviewSchema = z.object({
|
|
1489
1678
|
provider_npub: z.string(),
|
|
@@ -1684,7 +1873,7 @@ function makePaymentFeedbackHandler(opts) {
|
|
|
1684
1873
|
opts.resolveNoWallet(
|
|
1685
1874
|
`Payment required but no Solana wallet configured.
|
|
1686
1875
|
Amount: ${signedAmount !== void 0 ? formatAssetAmount(asset, BigInt(signedAmount)) : "unknown"}
|
|
1687
|
-
Payment request: ${paymentRequest}`
|
|
1876
|
+
Payment request: ${sanitizeUntrusted(paymentRequest, "structured").text}`
|
|
1688
1877
|
);
|
|
1689
1878
|
return;
|
|
1690
1879
|
}
|
|
@@ -1723,6 +1912,57 @@ Payment request: ${paymentRequest}`
|
|
|
1723
1912
|
};
|
|
1724
1913
|
return { onFeedback, onResultReceived };
|
|
1725
1914
|
}
|
|
1915
|
+
function formatFileResultMetadata(jobId, attachment) {
|
|
1916
|
+
const name = sanitizeField(attachment.name, 200);
|
|
1917
|
+
const mime = sanitizeField(attachment.mime, 100);
|
|
1918
|
+
const details = sanitizeUntrusted(
|
|
1919
|
+
`name: ${name}
|
|
1920
|
+
size: ${attachment.size} bytes
|
|
1921
|
+
type: ${mime}`,
|
|
1922
|
+
"text"
|
|
1923
|
+
).text;
|
|
1924
|
+
return `Job completed. The result is a FILE (not inlined here):
|
|
1925
|
+
${details}
|
|
1926
|
+
Download it with fetch_job_file(job_event_id="${jobId}", output_path="<local path>").`;
|
|
1927
|
+
}
|
|
1928
|
+
function decodeResultPreview(rawContent) {
|
|
1929
|
+
try {
|
|
1930
|
+
const decoded = decodeJobPayload(rawContent);
|
|
1931
|
+
if (decoded.attachment) {
|
|
1932
|
+
return `[file result: ${decoded.attachment.name} (${decoded.attachment.size} bytes). Download with fetch_job_file.]`;
|
|
1933
|
+
}
|
|
1934
|
+
return decoded.text ?? rawContent;
|
|
1935
|
+
} catch {
|
|
1936
|
+
return rawContent;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
async function prepareTextInput(agent, text) {
|
|
1940
|
+
if (utf8ByteLength(text) <= LIMITS.MAX_ENCRYPTED_INLINE_BYTES) {
|
|
1941
|
+
return { input: text };
|
|
1942
|
+
}
|
|
1943
|
+
const byteLength = utf8ByteLength(text);
|
|
1944
|
+
if (agent.agentDir === void 0) {
|
|
1945
|
+
return {
|
|
1946
|
+
error: `Input is ${byteLength} bytes, over the ${LIMITS.MAX_ENCRYPTED_INLINE_BYTES}-byte inline limit, so it must be sent via P2P transfer - which requires a persistent agent (this is an ephemeral session).`
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
try {
|
|
1950
|
+
const seeded = await ensureIrohTransport(agent).seedBytes(Buffer.from(text, "utf8"));
|
|
1951
|
+
return {
|
|
1952
|
+
input: "",
|
|
1953
|
+
attachment: {
|
|
1954
|
+
name: "input.txt",
|
|
1955
|
+
size: seeded.size,
|
|
1956
|
+
mime: "text/plain",
|
|
1957
|
+
transports: [{ kind: "iroh", ticket: seeded.ticket }]
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
} catch (e) {
|
|
1961
|
+
return {
|
|
1962
|
+
error: `Failed to seed input for transfer: ${e instanceof Error ? e.message : String(e)}`
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1726
1966
|
async function executeSubmitAndPay(ctx, agent, params) {
|
|
1727
1967
|
const ping = await agent.client.ping.pingAgent(params.providerPubkey, PRE_PING_TIMEOUT_MS);
|
|
1728
1968
|
if (!ping.online) {
|
|
@@ -1754,12 +1994,14 @@ async function executeSubmitAndPay(ctx, agent, params) {
|
|
|
1754
1994
|
input: params.input,
|
|
1755
1995
|
capability: params.dTag,
|
|
1756
1996
|
providerPubkey: params.providerPubkey,
|
|
1757
|
-
kindOffset: params.kindOffset
|
|
1997
|
+
kindOffset: params.kindOffset,
|
|
1998
|
+
attachment: params.attachment
|
|
1758
1999
|
});
|
|
1759
2000
|
let paymentSig;
|
|
1760
2001
|
let paidAmountSubunits;
|
|
1761
2002
|
let paidAssetKey;
|
|
1762
2003
|
let paymentWarnings = [];
|
|
2004
|
+
let resultAttachment;
|
|
1763
2005
|
try {
|
|
1764
2006
|
const result = await awaitJobResult(
|
|
1765
2007
|
agent,
|
|
@@ -1790,7 +2032,12 @@ async function executeSubmitAndPay(ctx, agent, params) {
|
|
|
1790
2032
|
providerPubkey: params.providerPubkey,
|
|
1791
2033
|
customerPublicKey: agent.identity.publicKey,
|
|
1792
2034
|
callbacks: {
|
|
1793
|
-
onResult(content) {
|
|
2035
|
+
onResult(content, _eventId, attachment) {
|
|
2036
|
+
if (attachment) {
|
|
2037
|
+
resultAttachment = attachment;
|
|
2038
|
+
payHandler.onResultReceived(formatFileResultMetadata(jobId, attachment));
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1794
2041
|
const kind = isLikelyBase64(content) ? "binary" : "text";
|
|
1795
2042
|
const sanitized = sanitizeUntrusted(content, kind);
|
|
1796
2043
|
payHandler.onResultReceived(`Job completed.
|
|
@@ -1799,7 +2046,7 @@ ${sanitized.text}`);
|
|
|
1799
2046
|
},
|
|
1800
2047
|
onFeedback: payHandler.onFeedback,
|
|
1801
2048
|
onError(error) {
|
|
1802
|
-
reject(new Error(`Job error: ${error}`));
|
|
2049
|
+
reject(new Error(`Job error: ${sanitizeUntrusted(error, "text").text}`));
|
|
1803
2050
|
},
|
|
1804
2051
|
onTimeout(timeoutMs) {
|
|
1805
2052
|
reject(new JobWaitTimeoutError(timeoutMs));
|
|
@@ -1822,7 +2069,8 @@ ${sanitized.text}`);
|
|
|
1822
2069
|
submittedAt,
|
|
1823
2070
|
completedAt: Date.now(),
|
|
1824
2071
|
resultPreview: result.slice(0, RESULT_PREVIEW_MAX_LEN),
|
|
1825
|
-
paymentSig
|
|
2072
|
+
paymentSig,
|
|
2073
|
+
attachmentJson: resultAttachment ? JSON.stringify(resultAttachment) : void 0
|
|
1826
2074
|
});
|
|
1827
2075
|
const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
|
|
1828
2076
|
` : "";
|
|
@@ -1953,7 +2201,11 @@ var customerTools = [
|
|
|
1953
2201
|
providerPubkey,
|
|
1954
2202
|
customerPublicKey: agent.identity.publicKey,
|
|
1955
2203
|
callbacks: {
|
|
1956
|
-
onResult(content, _eventId) {
|
|
2204
|
+
onResult(content, _eventId, attachment) {
|
|
2205
|
+
if (attachment) {
|
|
2206
|
+
resolve(formatFileResultMetadata(input.job_event_id, attachment));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
1957
2209
|
const kind = isLikelyBase64(content) ? "binary" : "text";
|
|
1958
2210
|
const sanitized = sanitizeUntrusted(content, kind);
|
|
1959
2211
|
resolve(sanitized.text);
|
|
@@ -1989,6 +2241,79 @@ var customerTools = [
|
|
|
1989
2241
|
return textResult(result);
|
|
1990
2242
|
}
|
|
1991
2243
|
}),
|
|
2244
|
+
defineTool({
|
|
2245
|
+
name: "fetch_job_file",
|
|
2246
|
+
description: "Download a job result that was delivered as a FILE (transferred P2P via iroh) to a local path. Use this after submit_and_pay_job or get_job_result reports a file result. Resumable and bounded by a max file size; the bytes are written to disk, never returned to you inline.",
|
|
2247
|
+
schema: FetchJobFileSchema,
|
|
2248
|
+
async handler(ctx, input) {
|
|
2249
|
+
checkLen("job_event_id", input.job_event_id, MAX_EVENT_ID_LEN);
|
|
2250
|
+
let outputPath;
|
|
2251
|
+
try {
|
|
2252
|
+
outputPath = await resolveOutputPath(input.output_path, {
|
|
2253
|
+
allowOutsideCwd: input.allow_outside_cwd
|
|
2254
|
+
});
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
return errorResult(error instanceof Error ? error.message : String(error));
|
|
2257
|
+
}
|
|
2258
|
+
const agent = ctx.active();
|
|
2259
|
+
let attachment;
|
|
2260
|
+
if (agent.agentDir !== void 0) {
|
|
2261
|
+
const entry = await findCustomerJob(agent.agentDir, input.job_event_id);
|
|
2262
|
+
if (entry?.attachmentJson !== void 0) {
|
|
2263
|
+
try {
|
|
2264
|
+
attachment = JSON.parse(entry.attachmentJson);
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
if (attachment === void 0) {
|
|
2270
|
+
try {
|
|
2271
|
+
const results = await agent.client.marketplace.queryJobResults(
|
|
2272
|
+
agent.identity,
|
|
2273
|
+
[input.job_event_id],
|
|
2274
|
+
[input.kind_offset]
|
|
2275
|
+
);
|
|
2276
|
+
const resultEntry = results.get(input.job_event_id);
|
|
2277
|
+
if (resultEntry !== void 0 && !resultEntry.decryptionFailed) {
|
|
2278
|
+
attachment = decodeJobPayload(resultEntry.content).attachment;
|
|
2279
|
+
}
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
logger.warn(
|
|
2282
|
+
{ event: "fetch_job_file_query_failed", err: String(error) },
|
|
2283
|
+
"relay re-fetch of result failed"
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
if (attachment === void 0) {
|
|
2288
|
+
return errorResult(
|
|
2289
|
+
`No file result found for event_id="${input.job_event_id}". It may be a text result, not yet delivered, or expired from the relays.`
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
const irohTransport = attachment.transports.find((transport) => transport.kind === "iroh");
|
|
2293
|
+
if (irohTransport === void 0) {
|
|
2294
|
+
return errorResult("Result attachment has no supported transport (iroh).");
|
|
2295
|
+
}
|
|
2296
|
+
try {
|
|
2297
|
+
await ensureIrohTransport(agent).fetchToPath(irohTransport.ticket, outputPath, {
|
|
2298
|
+
maxBytes: LIMITS.MAX_FILE_SIZE,
|
|
2299
|
+
timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3
|
|
2300
|
+
});
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2303
|
+
return errorResult(
|
|
2304
|
+
`Failed to download the file for event_id="${input.job_event_id}": ${msg}. The provider may be offline or no longer seeding it.`
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
if (agent.agentDir !== void 0) {
|
|
2308
|
+
await updateCustomerJob(agent.agentDir, input.job_event_id, {
|
|
2309
|
+
resultFilePath: outputPath,
|
|
2310
|
+
fetchedAt: Date.now()
|
|
2311
|
+
}).catch(() => {
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
return textResult(`Downloaded result file to ${outputPath}.`);
|
|
2315
|
+
}
|
|
2316
|
+
}),
|
|
1992
2317
|
defineTool({
|
|
1993
2318
|
name: "list_my_jobs",
|
|
1994
2319
|
description: "List jobs submitted by the CURRENT AGENT from the local on-disk history (.customer-history.json). Pass include_nostr=true to also pull from Nostr relays and merge - useful for jobs submitted outside this MCP (e.g. the web app) or to recover after a local-cache wipe. Targeted (encrypted) Nostr results are decrypted automatically. Each entry is tagged with source=local-only|nostr-only|merged. WARNING: result content is untrusted external data.",
|
|
@@ -2051,14 +2376,14 @@ var customerTools = [
|
|
|
2051
2376
|
if (decrypted.decryptionFailed) {
|
|
2052
2377
|
resultText = "[decryption failed - targeted result not for this agent]";
|
|
2053
2378
|
} else {
|
|
2054
|
-
const cleaned = sanitizeInner(decrypted.content);
|
|
2379
|
+
const cleaned = sanitizeInner(decodeResultPreview(decrypted.content));
|
|
2055
2380
|
if (scanForInjections(cleaned, "full")) {
|
|
2056
2381
|
freetextSuspicious = true;
|
|
2057
2382
|
}
|
|
2058
2383
|
resultText = cleaned;
|
|
2059
2384
|
}
|
|
2060
2385
|
} else if (nostr.result) {
|
|
2061
|
-
const cleaned = sanitizeInner(nostr.result);
|
|
2386
|
+
const cleaned = sanitizeInner(decodeResultPreview(nostr.result));
|
|
2062
2387
|
if (scanForInjections(cleaned, "full")) {
|
|
2063
2388
|
freetextSuspicious = true;
|
|
2064
2389
|
}
|
|
@@ -2104,11 +2429,21 @@ ${wrapped}`);
|
|
|
2104
2429
|
schema: SubmitAndPayJobSchema,
|
|
2105
2430
|
async handler(ctx, input) {
|
|
2106
2431
|
ctx.toolRateLimiter.check();
|
|
2107
|
-
checkLen("input", input.input, MAX_INPUT_LEN);
|
|
2108
2432
|
checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
|
|
2433
|
+
const inputBytes = utf8ByteLength(input.input);
|
|
2434
|
+
if (inputBytes > LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2435
|
+
return errorResult(
|
|
2436
|
+
`Input is ${inputBytes} bytes (max ${LIMITS.MAX_REINLINE_TEXT_BYTES} for an inline job). Send a large file with submit_and_pay_job_from_file.`
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2109
2439
|
const agent = ctx.active();
|
|
2440
|
+
const prepared = await prepareTextInput(agent, input.input);
|
|
2441
|
+
if ("error" in prepared) {
|
|
2442
|
+
return errorResult(prepared.error);
|
|
2443
|
+
}
|
|
2110
2444
|
return executeSubmitAndPay(ctx, agent, {
|
|
2111
|
-
input:
|
|
2445
|
+
input: prepared.input,
|
|
2446
|
+
attachment: prepared.attachment,
|
|
2112
2447
|
providerNpub: input.provider_npub,
|
|
2113
2448
|
providerPubkey: decodeNpub(input.provider_npub),
|
|
2114
2449
|
capability: input.capability,
|
|
@@ -2125,20 +2460,46 @@ ${wrapped}`);
|
|
|
2125
2460
|
}),
|
|
2126
2461
|
defineTool({
|
|
2127
2462
|
name: "submit_and_pay_job_from_file",
|
|
2128
|
-
description: "Same as submit_and_pay_job, but the job input is read from a file on disk by the MCP server instead of being passed inline by the LLM. Use this when the input is large (
|
|
2463
|
+
description: "Same as submit_and_pay_job, but the job input is read from a file on disk by the MCP server instead of being passed inline by the LLM. Use this when the input is large or binary (images, logs, captured output) and the LLM only needs to forward it - the file content never enters the model's output tokens. input_path may be absolute or relative to the MCP server's working directory. The file is ALWAYS transferred peer-to-peer via iroh, so this needs: a persistent agent, a PAID provider skill (free skills reject file inputs), and the iroh addon. Text files reach the skill on stdin; binary files via ELISYM_INPUT_FILE.",
|
|
2129
2464
|
schema: SubmitAndPayJobFromFileSchema,
|
|
2130
2465
|
async handler(ctx, input) {
|
|
2131
2466
|
ctx.toolRateLimiter.check();
|
|
2132
2467
|
checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
|
|
2133
|
-
let
|
|
2468
|
+
let prepared;
|
|
2134
2469
|
try {
|
|
2135
|
-
|
|
2470
|
+
prepared = await prepareFileInput(input.input_path, {
|
|
2471
|
+
allowOutsideCwd: input.allow_outside_cwd
|
|
2472
|
+
});
|
|
2136
2473
|
} catch (e) {
|
|
2137
2474
|
return errorResult(e instanceof Error ? e.message : String(e));
|
|
2138
2475
|
}
|
|
2139
2476
|
const agent = ctx.active();
|
|
2477
|
+
if (agent.agentDir === void 0) {
|
|
2478
|
+
return errorResult(
|
|
2479
|
+
`Sending a file requires a persistent agent (this is an ephemeral session). Files are always transferred P2P via iroh, never inline.`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
let attachment;
|
|
2483
|
+
try {
|
|
2484
|
+
const seeded = await ensureIrohTransport(agent).seedPath(prepared.absPath);
|
|
2485
|
+
attachment = {
|
|
2486
|
+
name: prepared.name,
|
|
2487
|
+
size: seeded.size,
|
|
2488
|
+
mime: prepared.mime,
|
|
2489
|
+
transports: [{ kind: "iroh", ticket: seeded.ticket }]
|
|
2490
|
+
};
|
|
2491
|
+
} catch (e) {
|
|
2492
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2493
|
+
if (/@number0\/iroh|iroh file transfer is unavailable/i.test(msg)) {
|
|
2494
|
+
return errorResult(
|
|
2495
|
+
`File transfer is unavailable: the optional @number0/iroh addon is not installed. Install it (e.g. \`bun add @number0/iroh\`) to send files.`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
return errorResult(`Failed to seed file for transfer: ${msg}`);
|
|
2499
|
+
}
|
|
2140
2500
|
return executeSubmitAndPay(ctx, agent, {
|
|
2141
|
-
input:
|
|
2501
|
+
input: "",
|
|
2502
|
+
attachment,
|
|
2142
2503
|
providerNpub: input.provider_npub,
|
|
2143
2504
|
providerPubkey: decodeNpub(input.provider_npub),
|
|
2144
2505
|
capability: input.capability,
|
|
@@ -2167,14 +2528,20 @@ ${wrapped}`);
|
|
|
2167
2528
|
` : "";
|
|
2168
2529
|
const payload = `${promptBlock}--- git diff (${diffResult.describedRange}) ---
|
|
2169
2530
|
${diffResult.diff}`;
|
|
2170
|
-
|
|
2531
|
+
const payloadBytes = utf8ByteLength(payload);
|
|
2532
|
+
if (payloadBytes > LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2171
2533
|
return errorResult(
|
|
2172
|
-
`Combined prompt + diff is ${
|
|
2534
|
+
`Combined prompt + diff is ${payloadBytes} bytes (max ${LIMITS.MAX_REINLINE_TEXT_BYTES}). Pass a narrower "base" or shorten the prompt.`
|
|
2173
2535
|
);
|
|
2174
2536
|
}
|
|
2175
2537
|
const agent = ctx.active();
|
|
2538
|
+
const prepared = await prepareTextInput(agent, payload);
|
|
2539
|
+
if ("error" in prepared) {
|
|
2540
|
+
return errorResult(prepared.error);
|
|
2541
|
+
}
|
|
2176
2542
|
return executeSubmitAndPay(ctx, agent, {
|
|
2177
|
-
input:
|
|
2543
|
+
input: prepared.input,
|
|
2544
|
+
attachment: prepared.attachment,
|
|
2178
2545
|
providerNpub: input.provider_npub,
|
|
2179
2546
|
providerPubkey: decodeNpub(input.provider_npub),
|
|
2180
2547
|
capability: input.capability,
|
|
@@ -2214,10 +2581,14 @@ ${diffResult.diff}`;
|
|
|
2214
2581
|
card = provider.cards[0];
|
|
2215
2582
|
}
|
|
2216
2583
|
if (!card) {
|
|
2217
|
-
const available = provider.cards.map(
|
|
2218
|
-
|
|
2219
|
-
|
|
2584
|
+
const available = provider.cards.map(
|
|
2585
|
+
(providerCard) => `${sanitizeField(providerCard.name ?? "", 64)} (${(providerCard.capabilities ?? []).map((capability) => sanitizeField(capability, 64)).join(", ")})`
|
|
2586
|
+
).join("; ");
|
|
2587
|
+
const { text } = sanitizeUntrusted(
|
|
2588
|
+
`No capability "${input.capability}" found for provider. Available: ${available}`,
|
|
2589
|
+
"text"
|
|
2220
2590
|
);
|
|
2591
|
+
return errorResult(text);
|
|
2221
2592
|
}
|
|
2222
2593
|
const price = card.payment?.job_price ?? 0;
|
|
2223
2594
|
const cardAsset = assetFromCardPayment(card.payment);
|
|
@@ -2228,13 +2599,18 @@ ${diffResult.diff}`;
|
|
|
2228
2599
|
}
|
|
2229
2600
|
if (price > 0 && input.max_price_lamports === void 0) {
|
|
2230
2601
|
const gasLine = await gasHintForCardAsset(agent, cardAsset);
|
|
2602
|
+
const safeProviderName = sanitizeField(provider.name || input.provider_npub, 64);
|
|
2603
|
+
const { text } = sanitizeUntrusted(
|
|
2604
|
+
`Capability "${input.capability}" from "${safeProviderName}" costs ${formatAssetAmount(cardAsset, BigInt(price))}.${gasLine}
|
|
2605
|
+
|
|
2606
|
+
To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`,
|
|
2607
|
+
"text"
|
|
2608
|
+
);
|
|
2231
2609
|
return {
|
|
2232
2610
|
content: [
|
|
2233
2611
|
{
|
|
2234
2612
|
type: "text",
|
|
2235
|
-
text
|
|
2236
|
-
|
|
2237
|
-
To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`
|
|
2613
|
+
text
|
|
2238
2614
|
}
|
|
2239
2615
|
]
|
|
2240
2616
|
};
|
|
@@ -2291,7 +2667,11 @@ To confirm, call buy_capability again with max_price_lamports set (e.g. ${price}
|
|
|
2291
2667
|
providerPubkey,
|
|
2292
2668
|
customerPublicKey: agent.identity.publicKey,
|
|
2293
2669
|
callbacks: {
|
|
2294
|
-
onResult(content) {
|
|
2670
|
+
onResult(content, _eventId, attachment) {
|
|
2671
|
+
if (attachment) {
|
|
2672
|
+
payHandler.onResultReceived(formatFileResultMetadata(jobId, attachment));
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2295
2675
|
const kind = isLikelyBase64(content) ? "binary" : "text";
|
|
2296
2676
|
const sanitized = sanitizeUntrusted(content, kind);
|
|
2297
2677
|
payHandler.onResultReceived(
|
|
@@ -2360,6 +2740,16 @@ ${result}${tip}`);
|
|
|
2360
2740
|
}
|
|
2361
2741
|
})
|
|
2362
2742
|
];
|
|
2743
|
+
var MAX_CAPABILITY_TAG_LEN = 64;
|
|
2744
|
+
function withTimeout(work, timeoutMs) {
|
|
2745
|
+
let timer;
|
|
2746
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
2747
|
+
timer = setTimeout(() => {
|
|
2748
|
+
reject(new Error(`dashboard query timed out after ${timeoutMs}ms`));
|
|
2749
|
+
}, timeoutMs);
|
|
2750
|
+
});
|
|
2751
|
+
return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
|
|
2752
|
+
}
|
|
2363
2753
|
var GetDashboardSchema = z.object({
|
|
2364
2754
|
top_n: z.number().int().min(1).max(100).default(10),
|
|
2365
2755
|
chain: z.enum(["solana"]).default("solana"),
|
|
@@ -2374,27 +2764,38 @@ var dashboardTools = [
|
|
|
2374
2764
|
async handler(ctx, input) {
|
|
2375
2765
|
const agent = ctx.active();
|
|
2376
2766
|
const network = input.network ?? agent.network;
|
|
2377
|
-
|
|
2767
|
+
let agents;
|
|
2768
|
+
try {
|
|
2769
|
+
agents = await withTimeout(
|
|
2770
|
+
agent.client.discovery.fetchAgents(network),
|
|
2771
|
+
input.timeout_secs * 1e3
|
|
2772
|
+
);
|
|
2773
|
+
} catch (e) {
|
|
2774
|
+
return textResult(e instanceof Error ? e.message : String(e));
|
|
2775
|
+
}
|
|
2378
2776
|
const filtered = agents.filter(
|
|
2379
|
-
(
|
|
2777
|
+
(candidate) => candidate.cards.some((card) => (card.payment?.chain ?? "solana") === input.chain)
|
|
2380
2778
|
);
|
|
2381
|
-
const rows = filtered.map((
|
|
2382
|
-
const mainCard =
|
|
2779
|
+
const rows = filtered.map((candidate) => {
|
|
2780
|
+
const mainCard = candidate.cards[0];
|
|
2383
2781
|
const mainAsset = assetFromCardPayment(mainCard?.payment);
|
|
2384
2782
|
const mainPrice = mainCard?.payment?.job_price;
|
|
2783
|
+
const capabilities = (mainCard?.capabilities ?? []).map((capability) => sanitizeField(capability, MAX_CAPABILITY_TAG_LEN)).join(", ");
|
|
2385
2784
|
return {
|
|
2386
|
-
name: sanitizeField(
|
|
2387
|
-
npub:
|
|
2388
|
-
capabilities
|
|
2785
|
+
name: sanitizeField(candidate.name || mainCard?.name || "unknown", 30),
|
|
2786
|
+
npub: candidate.npub,
|
|
2787
|
+
capabilities,
|
|
2389
2788
|
price: mainPrice ? formatAssetAmount(mainAsset, BigInt(mainPrice)) : "free",
|
|
2390
|
-
cards_count:
|
|
2789
|
+
cards_count: candidate.cards.length
|
|
2391
2790
|
};
|
|
2392
2791
|
}).slice(0, input.top_n);
|
|
2393
2792
|
if (rows.length === 0) {
|
|
2394
2793
|
return textResult(`No agents found on ${network} (${input.chain}).`);
|
|
2395
2794
|
}
|
|
2396
2795
|
const header = `elisym Network Dashboard (${network}, ${input.chain})`;
|
|
2397
|
-
const table = rows.map(
|
|
2796
|
+
const table = rows.map(
|
|
2797
|
+
(row, index) => `${index + 1}. ${row.name} | ${row.capabilities} | ${row.price} | ${row.npub}`
|
|
2798
|
+
).join("\n");
|
|
2398
2799
|
const { text } = sanitizeUntrusted(table, "structured");
|
|
2399
2800
|
return textResult(`${header}
|
|
2400
2801
|
${"=".repeat(header.length)}
|
|
@@ -2795,7 +3196,13 @@ var discoveryTools = [
|
|
|
2795
3196
|
asset_mint: asset.mint,
|
|
2796
3197
|
chain: card.payment?.chain,
|
|
2797
3198
|
network: card.payment?.network,
|
|
2798
|
-
network_fee_estimate_sol: gasEstimate
|
|
3199
|
+
network_fee_estimate_sol: gasEstimate,
|
|
3200
|
+
// File-exchange hints (dynamic-script). Informational: the MCP/CLI
|
|
3201
|
+
// CAN send files via submit_and_pay_job_from_file, so this does not
|
|
3202
|
+
// gate anything - it just tells the caller a file input is expected.
|
|
3203
|
+
// Already length-bounded by parseCapabilityEvent.
|
|
3204
|
+
...card.inputMime ? { input_mime: card.inputMime } : {},
|
|
3205
|
+
...card.outputMime ? { output_mime: card.outputMime } : {}
|
|
2799
3206
|
};
|
|
2800
3207
|
}),
|
|
2801
3208
|
supported_kinds: a.supportedKinds,
|
|
@@ -2971,7 +3378,8 @@ var feedbackContactsTools = [
|
|
|
2971
3378
|
contact.name ? ` name: ${contact.name}` : null,
|
|
2972
3379
|
contact.lastCapability ? ` last capability: ${contact.lastCapability}` : null
|
|
2973
3380
|
].filter((line) => line !== null);
|
|
2974
|
-
|
|
3381
|
+
const { text } = sanitizeUntrusted(lines.join("\n"), "text");
|
|
3382
|
+
return textResult(text);
|
|
2975
3383
|
}
|
|
2976
3384
|
}),
|
|
2977
3385
|
defineTool({
|
|
@@ -3044,18 +3452,26 @@ var policiesTools = [
|
|
|
3044
3452
|
}
|
|
3045
3453
|
const agent = ctx.active();
|
|
3046
3454
|
const policies = await agent.client.policies.fetchPolicies(pubkey);
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3455
|
+
let freetextSuspicious = false;
|
|
3456
|
+
const limited = policies.map((policy) => {
|
|
3457
|
+
const cleanedContent = sanitizeInner(policy.content);
|
|
3458
|
+
if (scanForInjections(cleanedContent, "full") || scanForInjections(policy.title ?? "", "full") || (policy.summary ? scanForInjections(policy.summary, "full") : false)) {
|
|
3459
|
+
freetextSuspicious = true;
|
|
3460
|
+
}
|
|
3461
|
+
return {
|
|
3462
|
+
type: policy.type,
|
|
3463
|
+
version: policy.version,
|
|
3464
|
+
title: sanitizeField(policy.title, LIMITS.MAX_POLICY_TITLE_LENGTH),
|
|
3465
|
+
summary: policy.summary ? sanitizeField(policy.summary, LIMITS.MAX_POLICY_SUMMARY_LENGTH) : void 0,
|
|
3466
|
+
content: cleanedContent,
|
|
3467
|
+
naddr: policy.naddr,
|
|
3468
|
+
published_at: policy.publishedAt
|
|
3469
|
+
};
|
|
3470
|
+
});
|
|
3056
3471
|
const { text } = sanitizeUntrusted(
|
|
3057
3472
|
JSON.stringify({ count: limited.length, policies: limited }, null, 2),
|
|
3058
|
-
"structured"
|
|
3473
|
+
"structured",
|
|
3474
|
+
{ extraInjectionSignal: freetextSuspicious }
|
|
3059
3475
|
);
|
|
3060
3476
|
return textResult(text);
|
|
3061
3477
|
}
|
|
@@ -3158,7 +3574,6 @@ var walletTools = [
|
|
|
3158
3574
|
const rpc = rpcFor(agent);
|
|
3159
3575
|
const walletAddress = address(agent.solanaKeypair.publicKey);
|
|
3160
3576
|
const { value: balanceLamports } = await rpc.getBalance(walletAddress).send();
|
|
3161
|
-
const balance = Number(balanceLamports);
|
|
3162
3577
|
const usdcBalanceRaw = await fetchUsdcBalance(rpc, walletAddress);
|
|
3163
3578
|
const usdcLine = `USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalanceRaw)}`;
|
|
3164
3579
|
const sessionLines = formatSessionSpendLines(ctx);
|
|
@@ -3167,7 +3582,7 @@ ${sessionLines.join("\n")}` : "";
|
|
|
3167
3582
|
return textResult(
|
|
3168
3583
|
`Address: ${agent.solanaKeypair.publicKey}
|
|
3169
3584
|
Network: ${agent.network}
|
|
3170
|
-
Balance: ${formatSol(
|
|
3585
|
+
Balance: ${formatSol(balanceLamports)} (${balanceLamports.toString()} lamports)
|
|
3171
3586
|
` + usdcLine + sessionBlock
|
|
3172
3587
|
);
|
|
3173
3588
|
}
|
|
@@ -3266,7 +3681,17 @@ Balance: ${formatSol(BigInt(balance))} (${balance} lamports)
|
|
|
3266
3681
|
releaseSpend(ctx, sendAsset, sendAmount);
|
|
3267
3682
|
throw e;
|
|
3268
3683
|
}
|
|
3269
|
-
|
|
3684
|
+
let remainingBalanceLine = "";
|
|
3685
|
+
try {
|
|
3686
|
+
const { value: balanceLamports } = await rpc.getBalance(address(agent.solanaKeypair.publicKey)).send();
|
|
3687
|
+
remainingBalanceLine = ` Remaining SOL balance: ${formatSol(balanceLamports)}
|
|
3688
|
+
`;
|
|
3689
|
+
} catch (e) {
|
|
3690
|
+
logger.warn(
|
|
3691
|
+
{ event: "post_payment_balance_fetch_failed", agent: agent.name },
|
|
3692
|
+
`Payment succeeded but the post-confirmation balance fetch failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3693
|
+
);
|
|
3694
|
+
}
|
|
3270
3695
|
const warnings = takeSpendWarnings(ctx, sendAsset);
|
|
3271
3696
|
for (const line of warnings) {
|
|
3272
3697
|
logger.warn({ event: "session_spend_threshold", agent: agent.name }, line);
|
|
@@ -3279,8 +3704,7 @@ Balance: ${formatSol(BigInt(balance))} (${balance} lamports)
|
|
|
3279
3704
|
Signature: ${signature}
|
|
3280
3705
|
Amount: ${formatAssetAmount(paidAsset, BigInt(requestData.amount))}
|
|
3281
3706
|
Recipient: ${requestData.recipient}
|
|
3282
|
-
|
|
3283
|
-
Explorer: ${explorerUrl(agent, signature)}`
|
|
3707
|
+
` + remainingBalanceLine + ` Explorer: ${explorerUrl(agent, signature)}`
|
|
3284
3708
|
);
|
|
3285
3709
|
}
|
|
3286
3710
|
}),
|
|
@@ -3586,10 +4010,21 @@ if (toolMap.size !== allTools.length) {
|
|
|
3586
4010
|
`Tool registry invariant violated: ${allTools.length} tools registered, ${toolMap.size} unique names`
|
|
3587
4011
|
);
|
|
3588
4012
|
}
|
|
4013
|
+
function redactSecrets(text) {
|
|
4014
|
+
return text.replace(/\bnsec1[02-9ac-hj-np-z]{20,}\b/gi, "[REDACTED]").replace(/\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}\b/g, "[REDACTED]").replace(/\b[0-9a-fA-F]{64}\b/g, "[REDACTED]").replace(/\b[1-9A-HJ-NP-Za-km-z]{80,}\b/g, "[REDACTED]").replace(/\[\s*(?:\d{1,3}\s*,\s*){31,}\d{1,3}\s*\]/g, "[REDACTED]");
|
|
4015
|
+
}
|
|
3589
4016
|
function safeError(context, e) {
|
|
3590
4017
|
const message = e instanceof Error ? e.message : String(e);
|
|
3591
4018
|
const stack = e instanceof Error ? e.stack : void 0;
|
|
3592
|
-
logger.error(
|
|
4019
|
+
logger.error(
|
|
4020
|
+
{
|
|
4021
|
+
event: "tool_error",
|
|
4022
|
+
context,
|
|
4023
|
+
err: redactSecrets(message),
|
|
4024
|
+
stack: stack !== void 0 ? redactSecrets(stack) : void 0
|
|
4025
|
+
},
|
|
4026
|
+
"tool call failed"
|
|
4027
|
+
);
|
|
3593
4028
|
let msg;
|
|
3594
4029
|
if (e instanceof ZodError) {
|
|
3595
4030
|
const parts = e.issues.map((i) => {
|
|
@@ -3598,12 +4033,15 @@ function safeError(context, e) {
|
|
|
3598
4033
|
});
|
|
3599
4034
|
msg = `Invalid arguments: ${parts.join("; ")}`;
|
|
3600
4035
|
} else if (e instanceof Error) {
|
|
3601
|
-
msg = e.message.split("\n")[0].slice(0, 300);
|
|
4036
|
+
msg = redactSecrets(e.message).split("\n")[0].slice(0, 300);
|
|
3602
4037
|
} else {
|
|
3603
|
-
msg = String(e).slice(0, 300);
|
|
4038
|
+
msg = redactSecrets(String(e)).split("\n")[0].slice(0, 300);
|
|
3604
4039
|
}
|
|
3605
4040
|
return {
|
|
3606
|
-
|
|
4041
|
+
// pino redact does not cover error-message string contents, so scrub key/secret
|
|
4042
|
+
// shapes from the LLM-facing message itself (e.g. a JSON parse error that echoes
|
|
4043
|
+
// a secrets file, or an RPC error embedding a key).
|
|
4044
|
+
content: [{ type: "text", text: redactSecrets(msg) }],
|
|
3607
4045
|
isError: true
|
|
3608
4046
|
};
|
|
3609
4047
|
}
|
|
@@ -3697,7 +4135,6 @@ async function startServer(ctx) {
|
|
|
3697
4135
|
}
|
|
3698
4136
|
const rpc = createSolanaRpc(rpcUrlFor(agent.network));
|
|
3699
4137
|
const { value: balanceLamports } = await rpc.getBalance(address(agent.solanaKeypair.publicKey)).send();
|
|
3700
|
-
const balance = Number(balanceLamports);
|
|
3701
4138
|
return {
|
|
3702
4139
|
contents: [
|
|
3703
4140
|
{
|
|
@@ -3707,8 +4144,8 @@ async function startServer(ctx) {
|
|
|
3707
4144
|
{
|
|
3708
4145
|
address: agent.solanaKeypair.publicKey,
|
|
3709
4146
|
network: agent.network,
|
|
3710
|
-
balance_lamports:
|
|
3711
|
-
balance_sol: formatSolNumeric(
|
|
4147
|
+
balance_lamports: balanceLamports.toString(),
|
|
4148
|
+
balance_sol: formatSolNumeric(balanceLamports),
|
|
3712
4149
|
chain: "solana"
|
|
3713
4150
|
},
|
|
3714
4151
|
null,
|
|
@@ -3728,6 +4165,7 @@ async function startServer(ctx) {
|
|
|
3728
4165
|
shuttingDown = true;
|
|
3729
4166
|
logger.info({ event: "shutdown", reason }, "shutting down");
|
|
3730
4167
|
for (const agent of ctx.registry.values()) {
|
|
4168
|
+
await shutdownIrohTransport(agent);
|
|
3731
4169
|
try {
|
|
3732
4170
|
agent.client.close();
|
|
3733
4171
|
} catch (e) {
|
|
@@ -3985,11 +4423,8 @@ function resolveAssetOrThrow(chain, token, mint) {
|
|
|
3985
4423
|
}
|
|
3986
4424
|
async function setSessionLimit(amount, chain, token, mint) {
|
|
3987
4425
|
const asset = resolveAssetOrThrow(chain, token, mint);
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {
|
|
3991
|
-
throw new Error(`amount "${amount}" must be a positive decimal`);
|
|
3992
|
-
}
|
|
4426
|
+
const trimmedAmount = amount.trim();
|
|
4427
|
+
parseAssetAmount(asset, trimmedAmount);
|
|
3993
4428
|
const path = globalConfigPath();
|
|
3994
4429
|
const cfg = await loadGlobalConfig(path);
|
|
3995
4430
|
const entries = cfg.session_spend_limits ? [...cfg.session_spend_limits] : [];
|
|
@@ -4001,7 +4436,7 @@ async function setSessionLimit(amount, chain, token, mint) {
|
|
|
4001
4436
|
chain: asset.chain,
|
|
4002
4437
|
token: asset.token,
|
|
4003
4438
|
mint: asset.mint,
|
|
4004
|
-
amount:
|
|
4439
|
+
amount: trimmedAmount
|
|
4005
4440
|
};
|
|
4006
4441
|
if (idx >= 0) {
|
|
4007
4442
|
entries[idx] = newEntry;
|
|
@@ -4010,7 +4445,7 @@ async function setSessionLimit(amount, chain, token, mint) {
|
|
|
4010
4445
|
}
|
|
4011
4446
|
await writeGlobalConfig(path, { session_spend_limits: entries });
|
|
4012
4447
|
console.log(
|
|
4013
|
-
`Session spend limit set to ${
|
|
4448
|
+
`Session spend limit set to ${trimmedAmount} ${asset.symbol} (process-wide). Restart the MCP server to apply.`
|
|
4014
4449
|
);
|
|
4015
4450
|
}
|
|
4016
4451
|
async function clearSessionLimit(chain, token, mint, all) {
|