@elisym/mcp 0.14.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 +371 -52
- 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, resolve, isAbsolute, relative } from 'node:path';
|
|
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",
|
|
@@ -1009,8 +1038,27 @@ Solana: ${solanaSigner.address}
|
|
|
1009
1038
|
];
|
|
1010
1039
|
var execFileP = promisify(execFile);
|
|
1011
1040
|
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([
|
|
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
|
+
]);
|
|
1014
1062
|
function isSensitiveInputPath(absPath) {
|
|
1015
1063
|
if (SENSITIVE_NAME_RE.test(absPath)) {
|
|
1016
1064
|
return true;
|
|
@@ -1021,8 +1069,38 @@ function isSensitiveInputPath(absPath) {
|
|
|
1021
1069
|
const segments = absPath.split(/[/\\]+/);
|
|
1022
1070
|
return segments.some((segment) => SENSITIVE_DIR_SEGMENTS.has(segment.toLowerCase()));
|
|
1023
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
|
+
}
|
|
1024
1102
|
var GIT_TIMEOUT_MS = 3e4;
|
|
1025
|
-
var GIT_MAX_BUFFER =
|
|
1103
|
+
var GIT_MAX_BUFFER = LIMITS.MAX_REINLINE_TEXT_BYTES + MAX_INPUT_LEN;
|
|
1026
1104
|
var GIT_SAFETY_ARGS = [
|
|
1027
1105
|
"-c",
|
|
1028
1106
|
"core.fsmonitor=",
|
|
@@ -1040,23 +1118,34 @@ function isValidGitRef(ref) {
|
|
|
1040
1118
|
}
|
|
1041
1119
|
return /^[A-Za-z0-9._/@~^-]+$/.test(ref);
|
|
1042
1120
|
}
|
|
1043
|
-
async function
|
|
1121
|
+
async function validateInputPath(inputPath, options) {
|
|
1044
1122
|
if (inputPath.length > MAX_INPUT_PATH_LEN) {
|
|
1045
1123
|
throw new Error(`input_path too long: ${inputPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`);
|
|
1046
1124
|
}
|
|
1047
1125
|
const cwd = resolve(process.cwd());
|
|
1048
|
-
const
|
|
1049
|
-
|
|
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)) {
|
|
1050
1138
|
throw new Error(
|
|
1051
1139
|
`Refusing to read a sensitive file as job input: ${absPath}. Secret keys, .env, SSH/keypair files, ~/.elisym and /proc are blocked.`
|
|
1052
1140
|
);
|
|
1053
1141
|
}
|
|
1054
1142
|
if (!options?.allowOutsideCwd) {
|
|
1055
|
-
const
|
|
1143
|
+
const realCwd = await realpath(cwd).catch(() => cwd);
|
|
1144
|
+
const rel = relative(realCwd, absPath);
|
|
1056
1145
|
const insideCwd = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1057
1146
|
if (!insideCwd) {
|
|
1058
1147
|
throw new Error(
|
|
1059
|
-
`input_path "${absPath}" resolves outside the working directory (${
|
|
1148
|
+
`input_path "${absPath}" resolves outside the working directory (${realCwd}). Move the file under the working directory or pass allow_outside_cwd: true.`
|
|
1060
1149
|
);
|
|
1061
1150
|
}
|
|
1062
1151
|
}
|
|
@@ -1073,18 +1162,36 @@ async function readJobInputFile(inputPath, options) {
|
|
|
1073
1162
|
if (!stats.isFile()) {
|
|
1074
1163
|
throw new Error(`input_path is not a regular file: ${absPath}`);
|
|
1075
1164
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
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;
|
|
1174
|
+
}
|
|
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.");
|
|
1080
1187
|
}
|
|
1081
|
-
|
|
1082
|
-
if (content.length > MAX_INPUT_LEN) {
|
|
1188
|
+
if (size > LIMITS.MAX_FILE_SIZE) {
|
|
1083
1189
|
throw new Error(
|
|
1084
|
-
`input_path
|
|
1190
|
+
`input_path too large: ${size} bytes (max ${LIMITS.MAX_FILE_SIZE} for a file transfer).`
|
|
1085
1191
|
);
|
|
1086
1192
|
}
|
|
1087
|
-
|
|
1193
|
+
const mime = await isProbablyText(absPath, size) ? "text/plain" : "application/octet-stream";
|
|
1194
|
+
return { absPath, size, name: basename(absPath), mime };
|
|
1088
1195
|
}
|
|
1089
1196
|
async function execGit(repoPath, args) {
|
|
1090
1197
|
try {
|
|
@@ -1151,18 +1258,18 @@ async function computeGitDiff(repoPath, base) {
|
|
|
1151
1258
|
`Invalid "base": ${base}. Use a branch/tag/commit ref (letters, digits, ". _ / @ ~ ^ -", no leading "-", no "..").`
|
|
1152
1259
|
);
|
|
1153
1260
|
}
|
|
1154
|
-
args = ["diff", "--no-ext-diff", "--end-of-options", `${base}...HEAD`];
|
|
1261
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "--end-of-options", `${base}...HEAD`];
|
|
1155
1262
|
describedRange = `${base}...HEAD`;
|
|
1156
1263
|
} else if (await isDirty(absRepo)) {
|
|
1157
|
-
args = ["diff", "--no-ext-diff", "HEAD"];
|
|
1264
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "HEAD"];
|
|
1158
1265
|
describedRange = "HEAD (working tree, uncommitted changes)";
|
|
1159
1266
|
} else {
|
|
1160
1267
|
const detected = await detectDefaultBase(absRepo);
|
|
1161
1268
|
if (detected) {
|
|
1162
|
-
args = ["diff", "--no-ext-diff", "--end-of-options", `${detected}...HEAD`];
|
|
1269
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "--end-of-options", `${detected}...HEAD`];
|
|
1163
1270
|
describedRange = `${detected}...HEAD`;
|
|
1164
1271
|
} else {
|
|
1165
|
-
args = ["diff", "--no-ext-diff", "HEAD"];
|
|
1272
|
+
args = ["diff", "--no-ext-diff", "--no-textconv", "HEAD"];
|
|
1166
1273
|
describedRange = "HEAD (no main/master detected)";
|
|
1167
1274
|
}
|
|
1168
1275
|
}
|
|
@@ -1172,9 +1279,10 @@ async function computeGitDiff(repoPath, base) {
|
|
|
1172
1279
|
`No changes in range ${describedRange}. Nothing to review - commit work, pass an explicit "base", or check the repo path.`
|
|
1173
1280
|
);
|
|
1174
1281
|
}
|
|
1175
|
-
|
|
1282
|
+
const diffBytes = utf8ByteLength(diff);
|
|
1283
|
+
if (diffBytes > LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
1176
1284
|
throw new Error(
|
|
1177
|
-
`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.`
|
|
1178
1286
|
);
|
|
1179
1287
|
}
|
|
1180
1288
|
return { diff, describedRange };
|
|
@@ -1416,7 +1524,13 @@ var CustomerJobEntrySchema = z.object({
|
|
|
1416
1524
|
completedAt: z.number().int().nonnegative(),
|
|
1417
1525
|
resultPreview: z.string().max(RESULT_PREVIEW_MAX_LEN).optional(),
|
|
1418
1526
|
paymentSig: z.string().max(128).optional(),
|
|
1419
|
-
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()
|
|
1420
1534
|
}).strict();
|
|
1421
1535
|
var CustomerHistorySchema = z.object({
|
|
1422
1536
|
version: z.literal(1),
|
|
@@ -1515,6 +1629,16 @@ var GetJobResultSchema = z.object({
|
|
|
1515
1629
|
timeout_secs: z.number().int().min(1).max(600).default(60),
|
|
1516
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.")
|
|
1517
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
|
+
});
|
|
1518
1642
|
var ListMyJobsSchema = z.object({
|
|
1519
1643
|
limit: z.number().int().min(1).max(50).default(20),
|
|
1520
1644
|
kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
|
|
@@ -1749,7 +1873,7 @@ function makePaymentFeedbackHandler(opts) {
|
|
|
1749
1873
|
opts.resolveNoWallet(
|
|
1750
1874
|
`Payment required but no Solana wallet configured.
|
|
1751
1875
|
Amount: ${signedAmount !== void 0 ? formatAssetAmount(asset, BigInt(signedAmount)) : "unknown"}
|
|
1752
|
-
Payment request: ${paymentRequest}`
|
|
1876
|
+
Payment request: ${sanitizeUntrusted(paymentRequest, "structured").text}`
|
|
1753
1877
|
);
|
|
1754
1878
|
return;
|
|
1755
1879
|
}
|
|
@@ -1788,6 +1912,57 @@ Payment request: ${paymentRequest}`
|
|
|
1788
1912
|
};
|
|
1789
1913
|
return { onFeedback, onResultReceived };
|
|
1790
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
|
+
}
|
|
1791
1966
|
async function executeSubmitAndPay(ctx, agent, params) {
|
|
1792
1967
|
const ping = await agent.client.ping.pingAgent(params.providerPubkey, PRE_PING_TIMEOUT_MS);
|
|
1793
1968
|
if (!ping.online) {
|
|
@@ -1819,12 +1994,14 @@ async function executeSubmitAndPay(ctx, agent, params) {
|
|
|
1819
1994
|
input: params.input,
|
|
1820
1995
|
capability: params.dTag,
|
|
1821
1996
|
providerPubkey: params.providerPubkey,
|
|
1822
|
-
kindOffset: params.kindOffset
|
|
1997
|
+
kindOffset: params.kindOffset,
|
|
1998
|
+
attachment: params.attachment
|
|
1823
1999
|
});
|
|
1824
2000
|
let paymentSig;
|
|
1825
2001
|
let paidAmountSubunits;
|
|
1826
2002
|
let paidAssetKey;
|
|
1827
2003
|
let paymentWarnings = [];
|
|
2004
|
+
let resultAttachment;
|
|
1828
2005
|
try {
|
|
1829
2006
|
const result = await awaitJobResult(
|
|
1830
2007
|
agent,
|
|
@@ -1855,7 +2032,12 @@ async function executeSubmitAndPay(ctx, agent, params) {
|
|
|
1855
2032
|
providerPubkey: params.providerPubkey,
|
|
1856
2033
|
customerPublicKey: agent.identity.publicKey,
|
|
1857
2034
|
callbacks: {
|
|
1858
|
-
onResult(content) {
|
|
2035
|
+
onResult(content, _eventId, attachment) {
|
|
2036
|
+
if (attachment) {
|
|
2037
|
+
resultAttachment = attachment;
|
|
2038
|
+
payHandler.onResultReceived(formatFileResultMetadata(jobId, attachment));
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1859
2041
|
const kind = isLikelyBase64(content) ? "binary" : "text";
|
|
1860
2042
|
const sanitized = sanitizeUntrusted(content, kind);
|
|
1861
2043
|
payHandler.onResultReceived(`Job completed.
|
|
@@ -1864,7 +2046,7 @@ ${sanitized.text}`);
|
|
|
1864
2046
|
},
|
|
1865
2047
|
onFeedback: payHandler.onFeedback,
|
|
1866
2048
|
onError(error) {
|
|
1867
|
-
reject(new Error(`Job error: ${error}`));
|
|
2049
|
+
reject(new Error(`Job error: ${sanitizeUntrusted(error, "text").text}`));
|
|
1868
2050
|
},
|
|
1869
2051
|
onTimeout(timeoutMs) {
|
|
1870
2052
|
reject(new JobWaitTimeoutError(timeoutMs));
|
|
@@ -1887,7 +2069,8 @@ ${sanitized.text}`);
|
|
|
1887
2069
|
submittedAt,
|
|
1888
2070
|
completedAt: Date.now(),
|
|
1889
2071
|
resultPreview: result.slice(0, RESULT_PREVIEW_MAX_LEN),
|
|
1890
|
-
paymentSig
|
|
2072
|
+
paymentSig,
|
|
2073
|
+
attachmentJson: resultAttachment ? JSON.stringify(resultAttachment) : void 0
|
|
1891
2074
|
});
|
|
1892
2075
|
const warningBlock = paymentWarnings.length > 0 ? `${paymentWarnings.join("\n")}
|
|
1893
2076
|
` : "";
|
|
@@ -2018,7 +2201,11 @@ var customerTools = [
|
|
|
2018
2201
|
providerPubkey,
|
|
2019
2202
|
customerPublicKey: agent.identity.publicKey,
|
|
2020
2203
|
callbacks: {
|
|
2021
|
-
onResult(content, _eventId) {
|
|
2204
|
+
onResult(content, _eventId, attachment) {
|
|
2205
|
+
if (attachment) {
|
|
2206
|
+
resolve(formatFileResultMetadata(input.job_event_id, attachment));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2022
2209
|
const kind = isLikelyBase64(content) ? "binary" : "text";
|
|
2023
2210
|
const sanitized = sanitizeUntrusted(content, kind);
|
|
2024
2211
|
resolve(sanitized.text);
|
|
@@ -2054,6 +2241,79 @@ var customerTools = [
|
|
|
2054
2241
|
return textResult(result);
|
|
2055
2242
|
}
|
|
2056
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
|
+
}),
|
|
2057
2317
|
defineTool({
|
|
2058
2318
|
name: "list_my_jobs",
|
|
2059
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.",
|
|
@@ -2116,14 +2376,14 @@ var customerTools = [
|
|
|
2116
2376
|
if (decrypted.decryptionFailed) {
|
|
2117
2377
|
resultText = "[decryption failed - targeted result not for this agent]";
|
|
2118
2378
|
} else {
|
|
2119
|
-
const cleaned = sanitizeInner(decrypted.content);
|
|
2379
|
+
const cleaned = sanitizeInner(decodeResultPreview(decrypted.content));
|
|
2120
2380
|
if (scanForInjections(cleaned, "full")) {
|
|
2121
2381
|
freetextSuspicious = true;
|
|
2122
2382
|
}
|
|
2123
2383
|
resultText = cleaned;
|
|
2124
2384
|
}
|
|
2125
2385
|
} else if (nostr.result) {
|
|
2126
|
-
const cleaned = sanitizeInner(nostr.result);
|
|
2386
|
+
const cleaned = sanitizeInner(decodeResultPreview(nostr.result));
|
|
2127
2387
|
if (scanForInjections(cleaned, "full")) {
|
|
2128
2388
|
freetextSuspicious = true;
|
|
2129
2389
|
}
|
|
@@ -2169,11 +2429,21 @@ ${wrapped}`);
|
|
|
2169
2429
|
schema: SubmitAndPayJobSchema,
|
|
2170
2430
|
async handler(ctx, input) {
|
|
2171
2431
|
ctx.toolRateLimiter.check();
|
|
2172
|
-
checkLen("input", input.input, MAX_INPUT_LEN);
|
|
2173
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
|
+
}
|
|
2174
2439
|
const agent = ctx.active();
|
|
2440
|
+
const prepared = await prepareTextInput(agent, input.input);
|
|
2441
|
+
if ("error" in prepared) {
|
|
2442
|
+
return errorResult(prepared.error);
|
|
2443
|
+
}
|
|
2175
2444
|
return executeSubmitAndPay(ctx, agent, {
|
|
2176
|
-
input:
|
|
2445
|
+
input: prepared.input,
|
|
2446
|
+
attachment: prepared.attachment,
|
|
2177
2447
|
providerNpub: input.provider_npub,
|
|
2178
2448
|
providerPubkey: decodeNpub(input.provider_npub),
|
|
2179
2449
|
capability: input.capability,
|
|
@@ -2190,22 +2460,46 @@ ${wrapped}`);
|
|
|
2190
2460
|
}),
|
|
2191
2461
|
defineTool({
|
|
2192
2462
|
name: "submit_and_pay_job_from_file",
|
|
2193
|
-
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.",
|
|
2194
2464
|
schema: SubmitAndPayJobFromFileSchema,
|
|
2195
2465
|
async handler(ctx, input) {
|
|
2196
2466
|
ctx.toolRateLimiter.check();
|
|
2197
2467
|
checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
|
|
2198
|
-
let
|
|
2468
|
+
let prepared;
|
|
2199
2469
|
try {
|
|
2200
|
-
|
|
2470
|
+
prepared = await prepareFileInput(input.input_path, {
|
|
2201
2471
|
allowOutsideCwd: input.allow_outside_cwd
|
|
2202
2472
|
});
|
|
2203
2473
|
} catch (e) {
|
|
2204
2474
|
return errorResult(e instanceof Error ? e.message : String(e));
|
|
2205
2475
|
}
|
|
2206
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
|
+
}
|
|
2207
2500
|
return executeSubmitAndPay(ctx, agent, {
|
|
2208
|
-
input:
|
|
2501
|
+
input: "",
|
|
2502
|
+
attachment,
|
|
2209
2503
|
providerNpub: input.provider_npub,
|
|
2210
2504
|
providerPubkey: decodeNpub(input.provider_npub),
|
|
2211
2505
|
capability: input.capability,
|
|
@@ -2234,14 +2528,20 @@ ${wrapped}`);
|
|
|
2234
2528
|
` : "";
|
|
2235
2529
|
const payload = `${promptBlock}--- git diff (${diffResult.describedRange}) ---
|
|
2236
2530
|
${diffResult.diff}`;
|
|
2237
|
-
|
|
2531
|
+
const payloadBytes = utf8ByteLength(payload);
|
|
2532
|
+
if (payloadBytes > LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2238
2533
|
return errorResult(
|
|
2239
|
-
`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.`
|
|
2240
2535
|
);
|
|
2241
2536
|
}
|
|
2242
2537
|
const agent = ctx.active();
|
|
2538
|
+
const prepared = await prepareTextInput(agent, payload);
|
|
2539
|
+
if ("error" in prepared) {
|
|
2540
|
+
return errorResult(prepared.error);
|
|
2541
|
+
}
|
|
2243
2542
|
return executeSubmitAndPay(ctx, agent, {
|
|
2244
|
-
input:
|
|
2543
|
+
input: prepared.input,
|
|
2544
|
+
attachment: prepared.attachment,
|
|
2245
2545
|
providerNpub: input.provider_npub,
|
|
2246
2546
|
providerPubkey: decodeNpub(input.provider_npub),
|
|
2247
2547
|
capability: input.capability,
|
|
@@ -2367,7 +2667,11 @@ To confirm, call buy_capability again with max_price_lamports set (e.g. ${price}
|
|
|
2367
2667
|
providerPubkey,
|
|
2368
2668
|
customerPublicKey: agent.identity.publicKey,
|
|
2369
2669
|
callbacks: {
|
|
2370
|
-
onResult(content) {
|
|
2670
|
+
onResult(content, _eventId, attachment) {
|
|
2671
|
+
if (attachment) {
|
|
2672
|
+
payHandler.onResultReceived(formatFileResultMetadata(jobId, attachment));
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2371
2675
|
const kind = isLikelyBase64(content) ? "binary" : "text";
|
|
2372
2676
|
const sanitized = sanitizeUntrusted(content, kind);
|
|
2373
2677
|
payHandler.onResultReceived(
|
|
@@ -2892,7 +3196,13 @@ var discoveryTools = [
|
|
|
2892
3196
|
asset_mint: asset.mint,
|
|
2893
3197
|
chain: card.payment?.chain,
|
|
2894
3198
|
network: card.payment?.network,
|
|
2895
|
-
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 } : {}
|
|
2896
3206
|
};
|
|
2897
3207
|
}),
|
|
2898
3208
|
supported_kinds: a.supportedKinds,
|
|
@@ -3701,12 +4011,20 @@ if (toolMap.size !== allTools.length) {
|
|
|
3701
4011
|
);
|
|
3702
4012
|
}
|
|
3703
4013
|
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]");
|
|
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]");
|
|
3705
4015
|
}
|
|
3706
4016
|
function safeError(context, e) {
|
|
3707
4017
|
const message = e instanceof Error ? e.message : String(e);
|
|
3708
4018
|
const stack = e instanceof Error ? e.stack : void 0;
|
|
3709
|
-
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
|
+
);
|
|
3710
4028
|
let msg;
|
|
3711
4029
|
if (e instanceof ZodError) {
|
|
3712
4030
|
const parts = e.issues.map((i) => {
|
|
@@ -3715,9 +4033,9 @@ function safeError(context, e) {
|
|
|
3715
4033
|
});
|
|
3716
4034
|
msg = `Invalid arguments: ${parts.join("; ")}`;
|
|
3717
4035
|
} else if (e instanceof Error) {
|
|
3718
|
-
msg = e.message.split("\n")[0].slice(0, 300);
|
|
4036
|
+
msg = redactSecrets(e.message).split("\n")[0].slice(0, 300);
|
|
3719
4037
|
} else {
|
|
3720
|
-
msg = String(e).slice(0, 300);
|
|
4038
|
+
msg = redactSecrets(String(e)).split("\n")[0].slice(0, 300);
|
|
3721
4039
|
}
|
|
3722
4040
|
return {
|
|
3723
4041
|
// pino redact does not cover error-message string contents, so scrub key/secret
|
|
@@ -3847,6 +4165,7 @@ async function startServer(ctx) {
|
|
|
3847
4165
|
shuttingDown = true;
|
|
3848
4166
|
logger.info({ event: "shutdown", reason }, "shutting down");
|
|
3849
4167
|
for (const agent of ctx.registry.values()) {
|
|
4168
|
+
await shutdownIrohTransport(agent);
|
|
3850
4169
|
try {
|
|
3851
4170
|
agent.client.close();
|
|
3852
4171
|
} catch (e) {
|