@elisym/mcp 0.12.1 → 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 +223 -102
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { LIMITS, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, RELAYS, toDTag, 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, 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
4
|
import { 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';
|
|
@@ -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(),
|
|
@@ -1536,9 +1601,6 @@ function buildJobCompletionTip(jobId, providerNpub) {
|
|
|
1536
1601
|
|
|
1537
1602
|
Tip: rate this provider with submit_feedback (job_event_id="${jobId}", rating="positive"|"negative"), or save them with add_contact (npub="${providerNpub}").`;
|
|
1538
1603
|
}
|
|
1539
|
-
function classifyJobFailure(message) {
|
|
1540
|
-
return /timed out/i.test(message) ? "timeout" : "failed";
|
|
1541
|
-
}
|
|
1542
1604
|
function pendingJobResult(jobId, paymentSig, submittedAt, warningBlock) {
|
|
1543
1605
|
const elapsedSecs = Math.round((Date.now() - submittedAt) / 1e3);
|
|
1544
1606
|
return textResult(
|
|
@@ -1803,6 +1865,9 @@ ${sanitized.text}`);
|
|
|
1803
1865
|
onFeedback: payHandler.onFeedback,
|
|
1804
1866
|
onError(error) {
|
|
1805
1867
|
reject(new Error(`Job error: ${error}`));
|
|
1868
|
+
},
|
|
1869
|
+
onTimeout(timeoutMs) {
|
|
1870
|
+
reject(new JobWaitTimeoutError(timeoutMs));
|
|
1806
1871
|
}
|
|
1807
1872
|
},
|
|
1808
1873
|
timeoutMs: params.timeoutMs,
|
|
@@ -1831,8 +1896,9 @@ ${sanitized.text}`);
|
|
|
1831
1896
|
${result}${tip}`);
|
|
1832
1897
|
} catch (e) {
|
|
1833
1898
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1834
|
-
const
|
|
1835
|
-
const
|
|
1899
|
+
const isTimeout = e instanceof JobWaitTimeoutError;
|
|
1900
|
+
const failure = isTimeout ? "timeout" : "failed";
|
|
1901
|
+
const pending = isTimeout && paymentSig !== void 0;
|
|
1836
1902
|
await recordJobOutcome(agent, {
|
|
1837
1903
|
jobEventId: jobId,
|
|
1838
1904
|
capability: params.dTag,
|
|
@@ -1893,10 +1959,7 @@ function awaitJobResult(agent, options, fn, safetyTimeoutMs) {
|
|
|
1893
1959
|
const resolvedOptions = fn({ resolve: safeResolve, reject: safeReject });
|
|
1894
1960
|
closeFn = agent.client.marketplace.subscribeToJobUpdates(resolvedOptions);
|
|
1895
1961
|
if (safetyTimeoutMs) {
|
|
1896
|
-
safetyTimer = setTimeout(
|
|
1897
|
-
() => safeReject(new Error("Subscription timed out (safety fallback).")),
|
|
1898
|
-
safetyTimeoutMs
|
|
1899
|
-
);
|
|
1962
|
+
safetyTimer = setTimeout(() => safeReject(new JobWaitTimeoutError()), safetyTimeoutMs);
|
|
1900
1963
|
}
|
|
1901
1964
|
});
|
|
1902
1965
|
}
|
|
@@ -1967,6 +2030,9 @@ var customerTools = [
|
|
|
1967
2030
|
},
|
|
1968
2031
|
onError(error) {
|
|
1969
2032
|
reject(new Error(`Job error: ${error}`));
|
|
2033
|
+
},
|
|
2034
|
+
onTimeout(timeoutMs) {
|
|
2035
|
+
reject(new JobWaitTimeoutError(timeoutMs));
|
|
1970
2036
|
}
|
|
1971
2037
|
},
|
|
1972
2038
|
timeoutMs: timeout,
|
|
@@ -1978,7 +2044,7 @@ var customerTools = [
|
|
|
1978
2044
|
);
|
|
1979
2045
|
} catch (e) {
|
|
1980
2046
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1981
|
-
if (
|
|
2047
|
+
if (e instanceof JobWaitTimeoutError) {
|
|
1982
2048
|
return textResult(
|
|
1983
2049
|
`event_id="${input.job_event_id}": result not ready yet (nothing within ${timeout / 1e3}s). This is NOT an error - the provider may still be working. Retry get_job_result later (optionally widen lookback_secs); results persist on the relays.`
|
|
1984
2050
|
);
|
|
@@ -2131,7 +2197,9 @@ ${wrapped}`);
|
|
|
2131
2197
|
checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
|
|
2132
2198
|
let payload;
|
|
2133
2199
|
try {
|
|
2134
|
-
payload = await readJobInputFile(input.input_path
|
|
2200
|
+
payload = await readJobInputFile(input.input_path, {
|
|
2201
|
+
allowOutsideCwd: input.allow_outside_cwd
|
|
2202
|
+
});
|
|
2135
2203
|
} catch (e) {
|
|
2136
2204
|
return errorResult(e instanceof Error ? e.message : String(e));
|
|
2137
2205
|
}
|
|
@@ -2213,10 +2281,14 @@ ${diffResult.diff}`;
|
|
|
2213
2281
|
card = provider.cards[0];
|
|
2214
2282
|
}
|
|
2215
2283
|
if (!card) {
|
|
2216
|
-
const available = provider.cards.map(
|
|
2217
|
-
|
|
2218
|
-
|
|
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"
|
|
2219
2290
|
);
|
|
2291
|
+
return errorResult(text);
|
|
2220
2292
|
}
|
|
2221
2293
|
const price = card.payment?.job_price ?? 0;
|
|
2222
2294
|
const cardAsset = assetFromCardPayment(card.payment);
|
|
@@ -2227,13 +2299,18 @@ ${diffResult.diff}`;
|
|
|
2227
2299
|
}
|
|
2228
2300
|
if (price > 0 && input.max_price_lamports === void 0) {
|
|
2229
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
|
+
);
|
|
2230
2309
|
return {
|
|
2231
2310
|
content: [
|
|
2232
2311
|
{
|
|
2233
2312
|
type: "text",
|
|
2234
|
-
text
|
|
2235
|
-
|
|
2236
|
-
To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`
|
|
2313
|
+
text
|
|
2237
2314
|
}
|
|
2238
2315
|
]
|
|
2239
2316
|
};
|
|
@@ -2302,6 +2379,9 @@ ${sanitized.text}`
|
|
|
2302
2379
|
onFeedback: payHandler.onFeedback,
|
|
2303
2380
|
onError(error) {
|
|
2304
2381
|
reject(new Error(`Job error: ${error}`));
|
|
2382
|
+
},
|
|
2383
|
+
onTimeout(timeoutMs) {
|
|
2384
|
+
reject(new JobWaitTimeoutError(timeoutMs));
|
|
2305
2385
|
}
|
|
2306
2386
|
},
|
|
2307
2387
|
timeoutMs: timeout,
|
|
@@ -2330,8 +2410,9 @@ ${sanitized.text}`
|
|
|
2330
2410
|
${result}${tip}`);
|
|
2331
2411
|
} catch (e) {
|
|
2332
2412
|
const msg = e instanceof Error ? e.message : String(e);
|
|
2333
|
-
const
|
|
2334
|
-
const
|
|
2413
|
+
const isTimeout = e instanceof JobWaitTimeoutError;
|
|
2414
|
+
const failure = isTimeout ? "timeout" : "failed";
|
|
2415
|
+
const pending = isTimeout && paymentSig !== void 0;
|
|
2335
2416
|
await recordJobOutcome(agent, {
|
|
2336
2417
|
jobEventId: jobId,
|
|
2337
2418
|
capability: dTag,
|
|
@@ -2355,6 +2436,16 @@ ${result}${tip}`);
|
|
|
2355
2436
|
}
|
|
2356
2437
|
})
|
|
2357
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
|
+
}
|
|
2358
2449
|
var GetDashboardSchema = z.object({
|
|
2359
2450
|
top_n: z.number().int().min(1).max(100).default(10),
|
|
2360
2451
|
chain: z.enum(["solana"]).default("solana"),
|
|
@@ -2369,27 +2460,38 @@ var dashboardTools = [
|
|
|
2369
2460
|
async handler(ctx, input) {
|
|
2370
2461
|
const agent = ctx.active();
|
|
2371
2462
|
const network = input.network ?? agent.network;
|
|
2372
|
-
|
|
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
|
+
}
|
|
2373
2472
|
const filtered = agents.filter(
|
|
2374
|
-
(
|
|
2473
|
+
(candidate) => candidate.cards.some((card) => (card.payment?.chain ?? "solana") === input.chain)
|
|
2375
2474
|
);
|
|
2376
|
-
const rows = filtered.map((
|
|
2377
|
-
const mainCard =
|
|
2475
|
+
const rows = filtered.map((candidate) => {
|
|
2476
|
+
const mainCard = candidate.cards[0];
|
|
2378
2477
|
const mainAsset = assetFromCardPayment(mainCard?.payment);
|
|
2379
2478
|
const mainPrice = mainCard?.payment?.job_price;
|
|
2479
|
+
const capabilities = (mainCard?.capabilities ?? []).map((capability) => sanitizeField(capability, MAX_CAPABILITY_TAG_LEN)).join(", ");
|
|
2380
2480
|
return {
|
|
2381
|
-
name: sanitizeField(
|
|
2382
|
-
npub:
|
|
2383
|
-
capabilities
|
|
2481
|
+
name: sanitizeField(candidate.name || mainCard?.name || "unknown", 30),
|
|
2482
|
+
npub: candidate.npub,
|
|
2483
|
+
capabilities,
|
|
2384
2484
|
price: mainPrice ? formatAssetAmount(mainAsset, BigInt(mainPrice)) : "free",
|
|
2385
|
-
cards_count:
|
|
2485
|
+
cards_count: candidate.cards.length
|
|
2386
2486
|
};
|
|
2387
2487
|
}).slice(0, input.top_n);
|
|
2388
2488
|
if (rows.length === 0) {
|
|
2389
2489
|
return textResult(`No agents found on ${network} (${input.chain}).`);
|
|
2390
2490
|
}
|
|
2391
2491
|
const header = `elisym Network Dashboard (${network}, ${input.chain})`;
|
|
2392
|
-
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");
|
|
2393
2495
|
const { text } = sanitizeUntrusted(table, "structured");
|
|
2394
2496
|
return textResult(`${header}
|
|
2395
2497
|
${"=".repeat(header.length)}
|
|
@@ -2966,7 +3068,8 @@ var feedbackContactsTools = [
|
|
|
2966
3068
|
contact.name ? ` name: ${contact.name}` : null,
|
|
2967
3069
|
contact.lastCapability ? ` last capability: ${contact.lastCapability}` : null
|
|
2968
3070
|
].filter((line) => line !== null);
|
|
2969
|
-
|
|
3071
|
+
const { text } = sanitizeUntrusted(lines.join("\n"), "text");
|
|
3072
|
+
return textResult(text);
|
|
2970
3073
|
}
|
|
2971
3074
|
}),
|
|
2972
3075
|
defineTool({
|
|
@@ -3039,18 +3142,26 @@ var policiesTools = [
|
|
|
3039
3142
|
}
|
|
3040
3143
|
const agent = ctx.active();
|
|
3041
3144
|
const policies = await agent.client.policies.fetchPolicies(pubkey);
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
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
|
+
});
|
|
3051
3161
|
const { text } = sanitizeUntrusted(
|
|
3052
3162
|
JSON.stringify({ count: limited.length, policies: limited }, null, 2),
|
|
3053
|
-
"structured"
|
|
3163
|
+
"structured",
|
|
3164
|
+
{ extraInjectionSignal: freetextSuspicious }
|
|
3054
3165
|
);
|
|
3055
3166
|
return textResult(text);
|
|
3056
3167
|
}
|
|
@@ -3153,7 +3264,6 @@ var walletTools = [
|
|
|
3153
3264
|
const rpc = rpcFor(agent);
|
|
3154
3265
|
const walletAddress = address(agent.solanaKeypair.publicKey);
|
|
3155
3266
|
const { value: balanceLamports } = await rpc.getBalance(walletAddress).send();
|
|
3156
|
-
const balance = Number(balanceLamports);
|
|
3157
3267
|
const usdcBalanceRaw = await fetchUsdcBalance(rpc, walletAddress);
|
|
3158
3268
|
const usdcLine = `USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalanceRaw)}`;
|
|
3159
3269
|
const sessionLines = formatSessionSpendLines(ctx);
|
|
@@ -3162,7 +3272,7 @@ ${sessionLines.join("\n")}` : "";
|
|
|
3162
3272
|
return textResult(
|
|
3163
3273
|
`Address: ${agent.solanaKeypair.publicKey}
|
|
3164
3274
|
Network: ${agent.network}
|
|
3165
|
-
Balance: ${formatSol(
|
|
3275
|
+
Balance: ${formatSol(balanceLamports)} (${balanceLamports.toString()} lamports)
|
|
3166
3276
|
` + usdcLine + sessionBlock
|
|
3167
3277
|
);
|
|
3168
3278
|
}
|
|
@@ -3261,7 +3371,17 @@ Balance: ${formatSol(BigInt(balance))} (${balance} lamports)
|
|
|
3261
3371
|
releaseSpend(ctx, sendAsset, sendAmount);
|
|
3262
3372
|
throw e;
|
|
3263
3373
|
}
|
|
3264
|
-
|
|
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
|
+
}
|
|
3265
3385
|
const warnings = takeSpendWarnings(ctx, sendAsset);
|
|
3266
3386
|
for (const line of warnings) {
|
|
3267
3387
|
logger.warn({ event: "session_spend_threshold", agent: agent.name }, line);
|
|
@@ -3274,8 +3394,7 @@ Balance: ${formatSol(BigInt(balance))} (${balance} lamports)
|
|
|
3274
3394
|
Signature: ${signature}
|
|
3275
3395
|
Amount: ${formatAssetAmount(paidAsset, BigInt(requestData.amount))}
|
|
3276
3396
|
Recipient: ${requestData.recipient}
|
|
3277
|
-
|
|
3278
|
-
Explorer: ${explorerUrl(agent, signature)}`
|
|
3397
|
+
` + remainingBalanceLine + ` Explorer: ${explorerUrl(agent, signature)}`
|
|
3279
3398
|
);
|
|
3280
3399
|
}
|
|
3281
3400
|
}),
|
|
@@ -3581,6 +3700,9 @@ if (toolMap.size !== allTools.length) {
|
|
|
3581
3700
|
`Tool registry invariant violated: ${allTools.length} tools registered, ${toolMap.size} unique names`
|
|
3582
3701
|
);
|
|
3583
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
|
+
}
|
|
3584
3706
|
function safeError(context, e) {
|
|
3585
3707
|
const message = e instanceof Error ? e.message : String(e);
|
|
3586
3708
|
const stack = e instanceof Error ? e.stack : void 0;
|
|
@@ -3598,7 +3720,10 @@ function safeError(context, e) {
|
|
|
3598
3720
|
msg = String(e).slice(0, 300);
|
|
3599
3721
|
}
|
|
3600
3722
|
return {
|
|
3601
|
-
|
|
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) }],
|
|
3602
3727
|
isError: true
|
|
3603
3728
|
};
|
|
3604
3729
|
}
|
|
@@ -3692,7 +3817,6 @@ async function startServer(ctx) {
|
|
|
3692
3817
|
}
|
|
3693
3818
|
const rpc = createSolanaRpc(rpcUrlFor(agent.network));
|
|
3694
3819
|
const { value: balanceLamports } = await rpc.getBalance(address(agent.solanaKeypair.publicKey)).send();
|
|
3695
|
-
const balance = Number(balanceLamports);
|
|
3696
3820
|
return {
|
|
3697
3821
|
contents: [
|
|
3698
3822
|
{
|
|
@@ -3702,8 +3826,8 @@ async function startServer(ctx) {
|
|
|
3702
3826
|
{
|
|
3703
3827
|
address: agent.solanaKeypair.publicKey,
|
|
3704
3828
|
network: agent.network,
|
|
3705
|
-
balance_lamports:
|
|
3706
|
-
balance_sol: formatSolNumeric(
|
|
3829
|
+
balance_lamports: balanceLamports.toString(),
|
|
3830
|
+
balance_sol: formatSolNumeric(balanceLamports),
|
|
3707
3831
|
chain: "solana"
|
|
3708
3832
|
},
|
|
3709
3833
|
null,
|
|
@@ -3980,11 +4104,8 @@ function resolveAssetOrThrow(chain, token, mint) {
|
|
|
3980
4104
|
}
|
|
3981
4105
|
async function setSessionLimit(amount, chain, token, mint) {
|
|
3982
4106
|
const asset = resolveAssetOrThrow(chain, token, mint);
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {
|
|
3986
|
-
throw new Error(`amount "${amount}" must be a positive decimal`);
|
|
3987
|
-
}
|
|
4107
|
+
const trimmedAmount = amount.trim();
|
|
4108
|
+
parseAssetAmount(asset, trimmedAmount);
|
|
3988
4109
|
const path = globalConfigPath();
|
|
3989
4110
|
const cfg = await loadGlobalConfig(path);
|
|
3990
4111
|
const entries = cfg.session_spend_limits ? [...cfg.session_spend_limits] : [];
|
|
@@ -3996,7 +4117,7 @@ async function setSessionLimit(amount, chain, token, mint) {
|
|
|
3996
4117
|
chain: asset.chain,
|
|
3997
4118
|
token: asset.token,
|
|
3998
4119
|
mint: asset.mint,
|
|
3999
|
-
amount:
|
|
4120
|
+
amount: trimmedAmount
|
|
4000
4121
|
};
|
|
4001
4122
|
if (idx >= 0) {
|
|
4002
4123
|
entries[idx] = newEntry;
|
|
@@ -4005,7 +4126,7 @@ async function setSessionLimit(amount, chain, token, mint) {
|
|
|
4005
4126
|
}
|
|
4006
4127
|
await writeGlobalConfig(path, { session_spend_limits: entries });
|
|
4007
4128
|
console.log(
|
|
4008
|
-
`Session spend limit set to ${
|
|
4129
|
+
`Session spend limit set to ${trimmedAmount} ${asset.symbol} (process-wide). Restart the MCP server to apply.`
|
|
4009
4130
|
);
|
|
4010
4131
|
}
|
|
4011
4132
|
async function clearSessionLimit(chain, token, mint, all) {
|