@elisym/mcp 0.13.0 → 0.14.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 +205 -89
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { Command } from 'commander';
|
|
|
7
7
|
import { generateSecretKey, nip19, getPublicKey } from 'nostr-tools';
|
|
8
8
|
import { stat, readFile, writeFile, rename, unlink } from 'node:fs/promises';
|
|
9
9
|
import { homedir, platform } from 'node:os';
|
|
10
|
-
import { dirname, join, isAbsolute,
|
|
10
|
+
import { dirname, join, resolve, isAbsolute, relative } from 'node:path';
|
|
11
11
|
import { readFileSync } from 'node:fs';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -725,7 +725,7 @@ async function buildEffectiveLimits() {
|
|
|
725
725
|
throw new Error(`Duplicate session_spend_limit entry in ${globalConfigPath()}: ${key}`);
|
|
726
726
|
}
|
|
727
727
|
seen.add(key);
|
|
728
|
-
map.set(key, parseAssetAmount(asset, entry.amount
|
|
728
|
+
map.set(key, parseAssetAmount(asset, entry.amount));
|
|
729
729
|
}
|
|
730
730
|
return map;
|
|
731
731
|
}
|
|
@@ -817,6 +817,14 @@ async function buildAgentInstance(name, config) {
|
|
|
817
817
|
agentDir: resolved?.dir
|
|
818
818
|
};
|
|
819
819
|
}
|
|
820
|
+
function scrubAgent(ctx, agent) {
|
|
821
|
+
agent.client.close();
|
|
822
|
+
if (agent.solanaKeypair) {
|
|
823
|
+
agent.solanaKeypair.secretKey.fill(0);
|
|
824
|
+
}
|
|
825
|
+
agent.identity.scrub();
|
|
826
|
+
ctx.registry.delete(agent.name);
|
|
827
|
+
}
|
|
820
828
|
var agentTools = [
|
|
821
829
|
defineTool({
|
|
822
830
|
name: "create_agent",
|
|
@@ -905,34 +913,34 @@ Solana: ${solanaSigner.address}
|
|
|
905
913
|
}
|
|
906
914
|
} catch {
|
|
907
915
|
}
|
|
916
|
+
let old;
|
|
908
917
|
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
|
-
}
|
|
918
|
+
old = ctx.active();
|
|
918
919
|
} catch {
|
|
919
920
|
}
|
|
920
921
|
if (ctx.registry.has(input.name)) {
|
|
922
|
+
if (old && old.name !== input.name) {
|
|
923
|
+
scrubAgent(ctx, old);
|
|
924
|
+
}
|
|
921
925
|
ctx.activeAgentName = input.name;
|
|
922
926
|
const agent = ctx.active();
|
|
923
|
-
const
|
|
924
|
-
return textResult(`Switched to agent "${input.name}" (${
|
|
927
|
+
const npub2 = agent.identity.npub;
|
|
928
|
+
return textResult(`Switched to agent "${input.name}" (${npub2}).`);
|
|
925
929
|
}
|
|
930
|
+
let instance;
|
|
926
931
|
try {
|
|
927
932
|
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}).`);
|
|
933
|
+
instance = await buildAgentInstance(input.name, config);
|
|
932
934
|
} catch (e) {
|
|
933
935
|
const msg = e instanceof Error ? e.message : String(e);
|
|
934
936
|
return errorResult(`Failed to load agent "${input.name}": ${msg}`);
|
|
935
937
|
}
|
|
938
|
+
if (old && old.name !== input.name) {
|
|
939
|
+
scrubAgent(ctx, old);
|
|
940
|
+
}
|
|
941
|
+
ctx.register(instance, true);
|
|
942
|
+
const npub = instance.identity.npub;
|
|
943
|
+
return textResult(`Loaded and switched to agent "${input.name}" (${npub}).`);
|
|
936
944
|
}
|
|
937
945
|
}),
|
|
938
946
|
defineTool({
|
|
@@ -994,25 +1002,64 @@ Solana: ${solanaSigner.address}
|
|
|
994
1002
|
if (!agent) {
|
|
995
1003
|
return errorResult(`Agent "${input.name}" is not loaded.`);
|
|
996
1004
|
}
|
|
997
|
-
agent
|
|
998
|
-
if (agent.solanaKeypair) {
|
|
999
|
-
agent.solanaKeypair.secretKey.fill(0);
|
|
1000
|
-
}
|
|
1001
|
-
agent.identity.scrub();
|
|
1002
|
-
ctx.registry.delete(input.name);
|
|
1005
|
+
scrubAgent(ctx, agent);
|
|
1003
1006
|
return textResult(`Agent "${input.name}" stopped and removed.`);
|
|
1004
1007
|
}
|
|
1005
1008
|
})
|
|
1006
1009
|
];
|
|
1007
1010
|
var execFileP = promisify(execFile);
|
|
1008
1011
|
var MAX_INPUT_PATH_LEN = 4096;
|
|
1012
|
+
var SENSITIVE_NAME_RE = /(^|[/\\])(\.secrets\.json|\.env(\..+)?|id_rsa|id_dsa|id_ecdsa|id_ed25519|.*-keypair\.json|.*\.pem|.*\.key)$/i;
|
|
1013
|
+
var SENSITIVE_DIR_SEGMENTS = /* @__PURE__ */ new Set([".elisym", ".ssh", ".aws", ".gnupg"]);
|
|
1014
|
+
function isSensitiveInputPath(absPath) {
|
|
1015
|
+
if (SENSITIVE_NAME_RE.test(absPath)) {
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
if (absPath === "/proc" || absPath.startsWith("/proc/")) {
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
const segments = absPath.split(/[/\\]+/);
|
|
1022
|
+
return segments.some((segment) => SENSITIVE_DIR_SEGMENTS.has(segment.toLowerCase()));
|
|
1023
|
+
}
|
|
1009
1024
|
var GIT_TIMEOUT_MS = 3e4;
|
|
1010
1025
|
var GIT_MAX_BUFFER = MAX_INPUT_LEN * 2;
|
|
1011
|
-
|
|
1026
|
+
var GIT_SAFETY_ARGS = [
|
|
1027
|
+
"-c",
|
|
1028
|
+
"core.fsmonitor=",
|
|
1029
|
+
"-c",
|
|
1030
|
+
"diff.external=",
|
|
1031
|
+
"-c",
|
|
1032
|
+
"core.hooksPath=/dev/null"
|
|
1033
|
+
];
|
|
1034
|
+
function isValidGitRef(ref) {
|
|
1035
|
+
if (ref.length === 0 || ref.length > 256) {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
if (ref.startsWith("-") || ref.includes("..")) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
return /^[A-Za-z0-9._/@~^-]+$/.test(ref);
|
|
1042
|
+
}
|
|
1043
|
+
async function readJobInputFile(inputPath, options) {
|
|
1012
1044
|
if (inputPath.length > MAX_INPUT_PATH_LEN) {
|
|
1013
1045
|
throw new Error(`input_path too long: ${inputPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`);
|
|
1014
1046
|
}
|
|
1015
|
-
const
|
|
1047
|
+
const cwd = resolve(process.cwd());
|
|
1048
|
+
const absPath = isAbsolute(inputPath) ? resolve(inputPath) : resolve(cwd, inputPath);
|
|
1049
|
+
if (isSensitiveInputPath(absPath)) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`Refusing to read a sensitive file as job input: ${absPath}. Secret keys, .env, SSH/keypair files, ~/.elisym and /proc are blocked.`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
if (!options?.allowOutsideCwd) {
|
|
1055
|
+
const rel = relative(cwd, absPath);
|
|
1056
|
+
const insideCwd = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1057
|
+
if (!insideCwd) {
|
|
1058
|
+
throw new Error(
|
|
1059
|
+
`input_path "${absPath}" resolves outside the working directory (${cwd}). Move the file under the working directory or pass allow_outside_cwd: true.`
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1016
1063
|
let stats;
|
|
1017
1064
|
try {
|
|
1018
1065
|
stats = await stat(absPath);
|
|
@@ -1041,11 +1088,11 @@ async function readJobInputFile(inputPath) {
|
|
|
1041
1088
|
}
|
|
1042
1089
|
async function execGit(repoPath, args) {
|
|
1043
1090
|
try {
|
|
1044
|
-
const { stdout } = await execFileP("git", args, {
|
|
1091
|
+
const { stdout } = await execFileP("git", [...GIT_SAFETY_ARGS, ...args], {
|
|
1045
1092
|
cwd: repoPath,
|
|
1046
1093
|
timeout: GIT_TIMEOUT_MS,
|
|
1047
1094
|
maxBuffer: GIT_MAX_BUFFER,
|
|
1048
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
1095
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_NOSYSTEM: "1" }
|
|
1049
1096
|
});
|
|
1050
1097
|
return stdout;
|
|
1051
1098
|
} catch (e) {
|
|
@@ -1099,18 +1146,23 @@ async function computeGitDiff(repoPath, base) {
|
|
|
1099
1146
|
let args;
|
|
1100
1147
|
let describedRange;
|
|
1101
1148
|
if (base) {
|
|
1102
|
-
|
|
1149
|
+
if (!isValidGitRef(base)) {
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
`Invalid "base": ${base}. Use a branch/tag/commit ref (letters, digits, ". _ / @ ~ ^ -", no leading "-", no "..").`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
args = ["diff", "--no-ext-diff", "--end-of-options", `${base}...HEAD`];
|
|
1103
1155
|
describedRange = `${base}...HEAD`;
|
|
1104
1156
|
} else if (await isDirty(absRepo)) {
|
|
1105
|
-
args = ["diff", "HEAD"];
|
|
1157
|
+
args = ["diff", "--no-ext-diff", "HEAD"];
|
|
1106
1158
|
describedRange = "HEAD (working tree, uncommitted changes)";
|
|
1107
1159
|
} else {
|
|
1108
1160
|
const detected = await detectDefaultBase(absRepo);
|
|
1109
1161
|
if (detected) {
|
|
1110
|
-
args = ["diff", `${detected}...HEAD`];
|
|
1162
|
+
args = ["diff", "--no-ext-diff", "--end-of-options", `${detected}...HEAD`];
|
|
1111
1163
|
describedRange = `${detected}...HEAD`;
|
|
1112
1164
|
} else {
|
|
1113
|
-
args = ["diff", "HEAD"];
|
|
1165
|
+
args = ["diff", "--no-ext-diff", "HEAD"];
|
|
1114
1166
|
describedRange = "HEAD (no main/master detected)";
|
|
1115
1167
|
}
|
|
1116
1168
|
}
|
|
@@ -1277,8 +1329,11 @@ var INJECTION_PATTERNS = [
|
|
|
1277
1329
|
var STRICT_INJECTION_PATTERNS = INJECTION_PATTERNS.filter((p) => !p.noisy);
|
|
1278
1330
|
function stripDangerousUnicode(text) {
|
|
1279
1331
|
return text.replace(
|
|
1332
|
+
// C0 (minus \n,\t) + C1 controls; bidi marks/overrides/isolates (061C, 200E-200F,
|
|
1333
|
+
// 202A-202E, 2066-2069); zero-width + word-joiner + invisible-operator format chars
|
|
1334
|
+
// (200B-200D, 2060-2064, 180E, FEFF); tag chars; replacement char.
|
|
1280
1335
|
// eslint-disable-next-line no-control-regex
|
|
1281
|
-
/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\u202A-\u202E\u2066-\u2069\
|
|
1336
|
+
/[\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
1337
|
""
|
|
1283
1338
|
);
|
|
1284
1339
|
}
|
|
@@ -1294,9 +1349,15 @@ function detectInjections(text, includeNoisy) {
|
|
|
1294
1349
|
if (normalized.length <= INJECTION_SCAN_BUDGET) {
|
|
1295
1350
|
return patterns.some((p) => p.pattern.test(normalized));
|
|
1296
1351
|
}
|
|
1297
|
-
const
|
|
1298
|
-
const
|
|
1299
|
-
|
|
1352
|
+
const OVERLAP = 256;
|
|
1353
|
+
const step = INJECTION_SCAN_BUDGET - OVERLAP;
|
|
1354
|
+
for (let start = 0; start < normalized.length; start += step) {
|
|
1355
|
+
const window = normalized.slice(start, start + INJECTION_SCAN_BUDGET);
|
|
1356
|
+
if (patterns.some((p) => p.pattern.test(window))) {
|
|
1357
|
+
return true;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
return false;
|
|
1300
1361
|
}
|
|
1301
1362
|
function scanForInjections(text, mode = "full") {
|
|
1302
1363
|
return detectInjections(text, mode === "full");
|
|
@@ -1305,8 +1366,11 @@ function isLikelyBase64(s) {
|
|
|
1305
1366
|
if (s.length < 64) {
|
|
1306
1367
|
return false;
|
|
1307
1368
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1369
|
+
if (/\s/.test(s)) {
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
const nonBase64Chars = s.replace(/[A-Za-z0-9+/=]/g, "");
|
|
1373
|
+
return nonBase64Chars.length / s.length < 0.02;
|
|
1310
1374
|
}
|
|
1311
1375
|
function sanitizeUntrusted(input, kind = "text", options) {
|
|
1312
1376
|
let text = stripDangerousUnicode(input);
|
|
@@ -1363,14 +1427,12 @@ var writeLocks = /* @__PURE__ */ new Map();
|
|
|
1363
1427
|
function withLock(path, fn) {
|
|
1364
1428
|
const previous = writeLocks.get(path) ?? Promise.resolve();
|
|
1365
1429
|
const next = previous.then(fn, fn);
|
|
1366
|
-
|
|
1367
|
-
path
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
})
|
|
1373
|
-
);
|
|
1430
|
+
const wrapped = next.finally(() => {
|
|
1431
|
+
if (writeLocks.get(path) === wrapped) {
|
|
1432
|
+
writeLocks.delete(path);
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
writeLocks.set(path, wrapped);
|
|
1374
1436
|
return next;
|
|
1375
1437
|
}
|
|
1376
1438
|
function pathFor(agentDir) {
|
|
@@ -1483,7 +1545,10 @@ var SubmitAndPayJobFromFileSchema = z.object({
|
|
|
1483
1545
|
capability: z.string().min(1).max(64).default("general"),
|
|
1484
1546
|
kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
|
|
1485
1547
|
timeout_secs: z.number().int().min(1).max(600).default(300),
|
|
1486
|
-
max_price_lamports: z.number().int().optional()
|
|
1548
|
+
max_price_lamports: z.number().int().optional(),
|
|
1549
|
+
allow_outside_cwd: z.boolean().default(false).describe(
|
|
1550
|
+
"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."
|
|
1551
|
+
)
|
|
1487
1552
|
});
|
|
1488
1553
|
var SubmitDiffReviewSchema = z.object({
|
|
1489
1554
|
provider_npub: z.string(),
|
|
@@ -2132,7 +2197,9 @@ ${wrapped}`);
|
|
|
2132
2197
|
checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
|
|
2133
2198
|
let payload;
|
|
2134
2199
|
try {
|
|
2135
|
-
payload = await readJobInputFile(input.input_path
|
|
2200
|
+
payload = await readJobInputFile(input.input_path, {
|
|
2201
|
+
allowOutsideCwd: input.allow_outside_cwd
|
|
2202
|
+
});
|
|
2136
2203
|
} catch (e) {
|
|
2137
2204
|
return errorResult(e instanceof Error ? e.message : String(e));
|
|
2138
2205
|
}
|
|
@@ -2214,10 +2281,14 @@ ${diffResult.diff}`;
|
|
|
2214
2281
|
card = provider.cards[0];
|
|
2215
2282
|
}
|
|
2216
2283
|
if (!card) {
|
|
2217
|
-
const available = provider.cards.map(
|
|
2218
|
-
|
|
2219
|
-
|
|
2284
|
+
const available = provider.cards.map(
|
|
2285
|
+
(providerCard) => `${sanitizeField(providerCard.name ?? "", 64)} (${(providerCard.capabilities ?? []).map((capability) => sanitizeField(capability, 64)).join(", ")})`
|
|
2286
|
+
).join("; ");
|
|
2287
|
+
const { text } = sanitizeUntrusted(
|
|
2288
|
+
`No capability "${input.capability}" found for provider. Available: ${available}`,
|
|
2289
|
+
"text"
|
|
2220
2290
|
);
|
|
2291
|
+
return errorResult(text);
|
|
2221
2292
|
}
|
|
2222
2293
|
const price = card.payment?.job_price ?? 0;
|
|
2223
2294
|
const cardAsset = assetFromCardPayment(card.payment);
|
|
@@ -2228,13 +2299,18 @@ ${diffResult.diff}`;
|
|
|
2228
2299
|
}
|
|
2229
2300
|
if (price > 0 && input.max_price_lamports === void 0) {
|
|
2230
2301
|
const gasLine = await gasHintForCardAsset(agent, cardAsset);
|
|
2302
|
+
const safeProviderName = sanitizeField(provider.name || input.provider_npub, 64);
|
|
2303
|
+
const { text } = sanitizeUntrusted(
|
|
2304
|
+
`Capability "${input.capability}" from "${safeProviderName}" costs ${formatAssetAmount(cardAsset, BigInt(price))}.${gasLine}
|
|
2305
|
+
|
|
2306
|
+
To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`,
|
|
2307
|
+
"text"
|
|
2308
|
+
);
|
|
2231
2309
|
return {
|
|
2232
2310
|
content: [
|
|
2233
2311
|
{
|
|
2234
2312
|
type: "text",
|
|
2235
|
-
text
|
|
2236
|
-
|
|
2237
|
-
To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`
|
|
2313
|
+
text
|
|
2238
2314
|
}
|
|
2239
2315
|
]
|
|
2240
2316
|
};
|
|
@@ -2360,6 +2436,16 @@ ${result}${tip}`);
|
|
|
2360
2436
|
}
|
|
2361
2437
|
})
|
|
2362
2438
|
];
|
|
2439
|
+
var MAX_CAPABILITY_TAG_LEN = 64;
|
|
2440
|
+
function withTimeout(work, timeoutMs) {
|
|
2441
|
+
let timer;
|
|
2442
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
2443
|
+
timer = setTimeout(() => {
|
|
2444
|
+
reject(new Error(`dashboard query timed out after ${timeoutMs}ms`));
|
|
2445
|
+
}, timeoutMs);
|
|
2446
|
+
});
|
|
2447
|
+
return Promise.race([work, timeout]).finally(() => clearTimeout(timer));
|
|
2448
|
+
}
|
|
2363
2449
|
var GetDashboardSchema = z.object({
|
|
2364
2450
|
top_n: z.number().int().min(1).max(100).default(10),
|
|
2365
2451
|
chain: z.enum(["solana"]).default("solana"),
|
|
@@ -2374,27 +2460,38 @@ var dashboardTools = [
|
|
|
2374
2460
|
async handler(ctx, input) {
|
|
2375
2461
|
const agent = ctx.active();
|
|
2376
2462
|
const network = input.network ?? agent.network;
|
|
2377
|
-
|
|
2463
|
+
let agents;
|
|
2464
|
+
try {
|
|
2465
|
+
agents = await withTimeout(
|
|
2466
|
+
agent.client.discovery.fetchAgents(network),
|
|
2467
|
+
input.timeout_secs * 1e3
|
|
2468
|
+
);
|
|
2469
|
+
} catch (e) {
|
|
2470
|
+
return textResult(e instanceof Error ? e.message : String(e));
|
|
2471
|
+
}
|
|
2378
2472
|
const filtered = agents.filter(
|
|
2379
|
-
(
|
|
2473
|
+
(candidate) => candidate.cards.some((card) => (card.payment?.chain ?? "solana") === input.chain)
|
|
2380
2474
|
);
|
|
2381
|
-
const rows = filtered.map((
|
|
2382
|
-
const mainCard =
|
|
2475
|
+
const rows = filtered.map((candidate) => {
|
|
2476
|
+
const mainCard = candidate.cards[0];
|
|
2383
2477
|
const mainAsset = assetFromCardPayment(mainCard?.payment);
|
|
2384
2478
|
const mainPrice = mainCard?.payment?.job_price;
|
|
2479
|
+
const capabilities = (mainCard?.capabilities ?? []).map((capability) => sanitizeField(capability, MAX_CAPABILITY_TAG_LEN)).join(", ");
|
|
2385
2480
|
return {
|
|
2386
|
-
name: sanitizeField(
|
|
2387
|
-
npub:
|
|
2388
|
-
capabilities
|
|
2481
|
+
name: sanitizeField(candidate.name || mainCard?.name || "unknown", 30),
|
|
2482
|
+
npub: candidate.npub,
|
|
2483
|
+
capabilities,
|
|
2389
2484
|
price: mainPrice ? formatAssetAmount(mainAsset, BigInt(mainPrice)) : "free",
|
|
2390
|
-
cards_count:
|
|
2485
|
+
cards_count: candidate.cards.length
|
|
2391
2486
|
};
|
|
2392
2487
|
}).slice(0, input.top_n);
|
|
2393
2488
|
if (rows.length === 0) {
|
|
2394
2489
|
return textResult(`No agents found on ${network} (${input.chain}).`);
|
|
2395
2490
|
}
|
|
2396
2491
|
const header = `elisym Network Dashboard (${network}, ${input.chain})`;
|
|
2397
|
-
const table = rows.map(
|
|
2492
|
+
const table = rows.map(
|
|
2493
|
+
(row, index) => `${index + 1}. ${row.name} | ${row.capabilities} | ${row.price} | ${row.npub}`
|
|
2494
|
+
).join("\n");
|
|
2398
2495
|
const { text } = sanitizeUntrusted(table, "structured");
|
|
2399
2496
|
return textResult(`${header}
|
|
2400
2497
|
${"=".repeat(header.length)}
|
|
@@ -2971,7 +3068,8 @@ var feedbackContactsTools = [
|
|
|
2971
3068
|
contact.name ? ` name: ${contact.name}` : null,
|
|
2972
3069
|
contact.lastCapability ? ` last capability: ${contact.lastCapability}` : null
|
|
2973
3070
|
].filter((line) => line !== null);
|
|
2974
|
-
|
|
3071
|
+
const { text } = sanitizeUntrusted(lines.join("\n"), "text");
|
|
3072
|
+
return textResult(text);
|
|
2975
3073
|
}
|
|
2976
3074
|
}),
|
|
2977
3075
|
defineTool({
|
|
@@ -3044,18 +3142,26 @@ var policiesTools = [
|
|
|
3044
3142
|
}
|
|
3045
3143
|
const agent = ctx.active();
|
|
3046
3144
|
const policies = await agent.client.policies.fetchPolicies(pubkey);
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3145
|
+
let freetextSuspicious = false;
|
|
3146
|
+
const limited = policies.map((policy) => {
|
|
3147
|
+
const cleanedContent = sanitizeInner(policy.content);
|
|
3148
|
+
if (scanForInjections(cleanedContent, "full") || scanForInjections(policy.title ?? "", "full") || (policy.summary ? scanForInjections(policy.summary, "full") : false)) {
|
|
3149
|
+
freetextSuspicious = true;
|
|
3150
|
+
}
|
|
3151
|
+
return {
|
|
3152
|
+
type: policy.type,
|
|
3153
|
+
version: policy.version,
|
|
3154
|
+
title: sanitizeField(policy.title, LIMITS.MAX_POLICY_TITLE_LENGTH),
|
|
3155
|
+
summary: policy.summary ? sanitizeField(policy.summary, LIMITS.MAX_POLICY_SUMMARY_LENGTH) : void 0,
|
|
3156
|
+
content: cleanedContent,
|
|
3157
|
+
naddr: policy.naddr,
|
|
3158
|
+
published_at: policy.publishedAt
|
|
3159
|
+
};
|
|
3160
|
+
});
|
|
3056
3161
|
const { text } = sanitizeUntrusted(
|
|
3057
3162
|
JSON.stringify({ count: limited.length, policies: limited }, null, 2),
|
|
3058
|
-
"structured"
|
|
3163
|
+
"structured",
|
|
3164
|
+
{ extraInjectionSignal: freetextSuspicious }
|
|
3059
3165
|
);
|
|
3060
3166
|
return textResult(text);
|
|
3061
3167
|
}
|
|
@@ -3158,7 +3264,6 @@ var walletTools = [
|
|
|
3158
3264
|
const rpc = rpcFor(agent);
|
|
3159
3265
|
const walletAddress = address(agent.solanaKeypair.publicKey);
|
|
3160
3266
|
const { value: balanceLamports } = await rpc.getBalance(walletAddress).send();
|
|
3161
|
-
const balance = Number(balanceLamports);
|
|
3162
3267
|
const usdcBalanceRaw = await fetchUsdcBalance(rpc, walletAddress);
|
|
3163
3268
|
const usdcLine = `USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalanceRaw)}`;
|
|
3164
3269
|
const sessionLines = formatSessionSpendLines(ctx);
|
|
@@ -3167,7 +3272,7 @@ ${sessionLines.join("\n")}` : "";
|
|
|
3167
3272
|
return textResult(
|
|
3168
3273
|
`Address: ${agent.solanaKeypair.publicKey}
|
|
3169
3274
|
Network: ${agent.network}
|
|
3170
|
-
Balance: ${formatSol(
|
|
3275
|
+
Balance: ${formatSol(balanceLamports)} (${balanceLamports.toString()} lamports)
|
|
3171
3276
|
` + usdcLine + sessionBlock
|
|
3172
3277
|
);
|
|
3173
3278
|
}
|
|
@@ -3266,7 +3371,17 @@ Balance: ${formatSol(BigInt(balance))} (${balance} lamports)
|
|
|
3266
3371
|
releaseSpend(ctx, sendAsset, sendAmount);
|
|
3267
3372
|
throw e;
|
|
3268
3373
|
}
|
|
3269
|
-
|
|
3374
|
+
let remainingBalanceLine = "";
|
|
3375
|
+
try {
|
|
3376
|
+
const { value: balanceLamports } = await rpc.getBalance(address(agent.solanaKeypair.publicKey)).send();
|
|
3377
|
+
remainingBalanceLine = ` Remaining SOL balance: ${formatSol(balanceLamports)}
|
|
3378
|
+
`;
|
|
3379
|
+
} catch (e) {
|
|
3380
|
+
logger.warn(
|
|
3381
|
+
{ event: "post_payment_balance_fetch_failed", agent: agent.name },
|
|
3382
|
+
`Payment succeeded but the post-confirmation balance fetch failed: ${e instanceof Error ? e.message : String(e)}`
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3270
3385
|
const warnings = takeSpendWarnings(ctx, sendAsset);
|
|
3271
3386
|
for (const line of warnings) {
|
|
3272
3387
|
logger.warn({ event: "session_spend_threshold", agent: agent.name }, line);
|
|
@@ -3279,8 +3394,7 @@ Balance: ${formatSol(BigInt(balance))} (${balance} lamports)
|
|
|
3279
3394
|
Signature: ${signature}
|
|
3280
3395
|
Amount: ${formatAssetAmount(paidAsset, BigInt(requestData.amount))}
|
|
3281
3396
|
Recipient: ${requestData.recipient}
|
|
3282
|
-
|
|
3283
|
-
Explorer: ${explorerUrl(agent, signature)}`
|
|
3397
|
+
` + remainingBalanceLine + ` Explorer: ${explorerUrl(agent, signature)}`
|
|
3284
3398
|
);
|
|
3285
3399
|
}
|
|
3286
3400
|
}),
|
|
@@ -3586,6 +3700,9 @@ if (toolMap.size !== allTools.length) {
|
|
|
3586
3700
|
`Tool registry invariant violated: ${allTools.length} tools registered, ${toolMap.size} unique names`
|
|
3587
3701
|
);
|
|
3588
3702
|
}
|
|
3703
|
+
function redactSecrets(text) {
|
|
3704
|
+
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]");
|
|
3705
|
+
}
|
|
3589
3706
|
function safeError(context, e) {
|
|
3590
3707
|
const message = e instanceof Error ? e.message : String(e);
|
|
3591
3708
|
const stack = e instanceof Error ? e.stack : void 0;
|
|
@@ -3603,7 +3720,10 @@ function safeError(context, e) {
|
|
|
3603
3720
|
msg = String(e).slice(0, 300);
|
|
3604
3721
|
}
|
|
3605
3722
|
return {
|
|
3606
|
-
|
|
3723
|
+
// pino redact does not cover error-message string contents, so scrub key/secret
|
|
3724
|
+
// shapes from the LLM-facing message itself (e.g. a JSON parse error that echoes
|
|
3725
|
+
// a secrets file, or an RPC error embedding a key).
|
|
3726
|
+
content: [{ type: "text", text: redactSecrets(msg) }],
|
|
3607
3727
|
isError: true
|
|
3608
3728
|
};
|
|
3609
3729
|
}
|
|
@@ -3697,7 +3817,6 @@ async function startServer(ctx) {
|
|
|
3697
3817
|
}
|
|
3698
3818
|
const rpc = createSolanaRpc(rpcUrlFor(agent.network));
|
|
3699
3819
|
const { value: balanceLamports } = await rpc.getBalance(address(agent.solanaKeypair.publicKey)).send();
|
|
3700
|
-
const balance = Number(balanceLamports);
|
|
3701
3820
|
return {
|
|
3702
3821
|
contents: [
|
|
3703
3822
|
{
|
|
@@ -3707,8 +3826,8 @@ async function startServer(ctx) {
|
|
|
3707
3826
|
{
|
|
3708
3827
|
address: agent.solanaKeypair.publicKey,
|
|
3709
3828
|
network: agent.network,
|
|
3710
|
-
balance_lamports:
|
|
3711
|
-
balance_sol: formatSolNumeric(
|
|
3829
|
+
balance_lamports: balanceLamports.toString(),
|
|
3830
|
+
balance_sol: formatSolNumeric(balanceLamports),
|
|
3712
3831
|
chain: "solana"
|
|
3713
3832
|
},
|
|
3714
3833
|
null,
|
|
@@ -3985,11 +4104,8 @@ function resolveAssetOrThrow(chain, token, mint) {
|
|
|
3985
4104
|
}
|
|
3986
4105
|
async function setSessionLimit(amount, chain, token, mint) {
|
|
3987
4106
|
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
|
-
}
|
|
4107
|
+
const trimmedAmount = amount.trim();
|
|
4108
|
+
parseAssetAmount(asset, trimmedAmount);
|
|
3993
4109
|
const path = globalConfigPath();
|
|
3994
4110
|
const cfg = await loadGlobalConfig(path);
|
|
3995
4111
|
const entries = cfg.session_spend_limits ? [...cfg.session_spend_limits] : [];
|
|
@@ -4001,7 +4117,7 @@ async function setSessionLimit(amount, chain, token, mint) {
|
|
|
4001
4117
|
chain: asset.chain,
|
|
4002
4118
|
token: asset.token,
|
|
4003
4119
|
mint: asset.mint,
|
|
4004
|
-
amount:
|
|
4120
|
+
amount: trimmedAmount
|
|
4005
4121
|
};
|
|
4006
4122
|
if (idx >= 0) {
|
|
4007
4123
|
entries[idx] = newEntry;
|
|
@@ -4010,7 +4126,7 @@ async function setSessionLimit(amount, chain, token, mint) {
|
|
|
4010
4126
|
}
|
|
4011
4127
|
await writeGlobalConfig(path, { session_spend_limits: entries });
|
|
4012
4128
|
console.log(
|
|
4013
|
-
`Session spend limit set to ${
|
|
4129
|
+
`Session spend limit set to ${trimmedAmount} ${asset.symbol} (process-wide). Restart the MCP server to apply.`
|
|
4014
4130
|
);
|
|
4015
4131
|
}
|
|
4016
4132
|
async function clearSessionLimit(chain, token, mint, all) {
|