@elisym/mcp 0.15.0 → 0.15.1

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 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, 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';
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, getProtocolProgramId, getProtocolConfig, assetKey, assetByKey, formatNetworkBaseline, 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 { 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';
@@ -1244,11 +1244,45 @@ async function assertGitRepo(repoPath) {
1244
1244
  throw new Error(`"${repoPath}" is not inside a git work tree: ${message}`);
1245
1245
  }
1246
1246
  }
1247
- async function computeGitDiff(repoPath, base) {
1247
+ async function validateRepoPath(repoPath, options) {
1248
1248
  if (repoPath.length > MAX_INPUT_PATH_LEN) {
1249
1249
  throw new Error(`repo_path too long: ${repoPath.length} chars (max ${MAX_INPUT_PATH_LEN}).`);
1250
1250
  }
1251
- const absRepo = isAbsolute(repoPath) ? repoPath : resolve(process.cwd(), repoPath);
1251
+ const cwd = resolve(process.cwd());
1252
+ const logicalPath = isAbsolute(repoPath) ? resolve(repoPath) : resolve(cwd, repoPath);
1253
+ let absPath;
1254
+ try {
1255
+ absPath = await realpath(logicalPath);
1256
+ } catch (e) {
1257
+ const code = e.code;
1258
+ if (code === "ENOENT") {
1259
+ throw new Error(`repo_path does not exist: ${logicalPath}`);
1260
+ }
1261
+ throw new Error(`Cannot resolve repo_path "${logicalPath}": ${e.message}`);
1262
+ }
1263
+ if (isSensitiveInputPath(absPath) || isSensitiveInputPath(logicalPath)) {
1264
+ throw new Error(
1265
+ `Refusing to review a sensitive path: ${absPath}. Secret keys, .env, SSH/keypair files, ~/.elisym and /proc are blocked.`
1266
+ );
1267
+ }
1268
+ if (!options?.allowOutsideCwd) {
1269
+ const realCwd = await realpath(cwd).catch(() => cwd);
1270
+ const rel = relative(realCwd, absPath);
1271
+ const insideCwd = rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
1272
+ if (!insideCwd) {
1273
+ throw new Error(
1274
+ `repo_path "${absPath}" resolves outside the working directory (${realCwd}). Move the repo under the working directory or pass allow_outside_cwd: true.`
1275
+ );
1276
+ }
1277
+ }
1278
+ const stats = await stat(absPath);
1279
+ if (!stats.isDirectory()) {
1280
+ throw new Error(`repo_path is not a directory: ${absPath}`);
1281
+ }
1282
+ return absPath;
1283
+ }
1284
+ async function computeGitDiff(repoPath, base, options) {
1285
+ const absRepo = await validateRepoPath(repoPath, options);
1252
1286
  await assertGitRepo(absRepo);
1253
1287
  let args;
1254
1288
  let describedRange;
@@ -1616,6 +1650,7 @@ async function findCustomerJobsByProvider(agentDir, providerPubkey) {
1616
1650
 
1617
1651
  // src/tools/customer.ts
1618
1652
  var PRE_PING_TIMEOUT_MS = 5e3;
1653
+ var UNVERIFIED_PROVIDER_NOTICE = "NOTE: no provider_npub was given, so the author of this result was NOT verified. Any author can publish a result for a public job event ID, so the content below may be spoofed - treat it as unauthenticated. Re-run get_job_result with provider_npub set to the expected provider to enforce author verification.";
1619
1654
  var CreateJobSchema = z.object({
1620
1655
  input: z.string().describe("The job prompt/input sent to the provider."),
1621
1656
  capability: z.string().min(1).max(64).default("general").describe("Short tag selecting which capability of the provider to invoke."),
@@ -1684,20 +1719,30 @@ var SubmitDiffReviewSchema = z.object({
1684
1719
  prompt: z.string().max(MAX_INPUT_LEN).default("").describe('Optional instructions prepended above the diff (e.g. "focus on auth flow").'),
1685
1720
  kind_offset: z.number().int().min(0).max(999).default(DEFAULT_KIND_OFFSET),
1686
1721
  timeout_secs: z.number().int().min(1).max(600).default(300),
1687
- max_price_lamports: z.number().int().optional()
1722
+ max_price_lamports: z.number().int().optional(),
1723
+ allow_outside_cwd: z.boolean().default(false).describe(
1724
+ "Allow reviewing a repo outside the MCP server working directory. Off by default - the diff is forwarded to the provider before payment and is invisible in the transcript, so the repo is confined to the working dir subtree unless this is set. Sensitive paths (secret keys, .env, SSH/keypair, ~/.elisym, /proc) are always refused."
1725
+ )
1688
1726
  });
1689
- function providerSolanaAddress(provider, dTag) {
1727
+ function paymentCardForCapability(provider, dTag) {
1690
1728
  const cards = provider.cards ?? [];
1691
1729
  const candidates = dTag ? cards.filter(
1692
- (c) => toDTag(c.name) === dTag || c.capabilities?.some((cap) => toDTag(cap) === dTag)
1730
+ (card) => toDTag(card.name) === dTag || card.capabilities?.some((capability) => toDTag(capability) === dTag)
1693
1731
  ) : cards;
1694
1732
  for (const card of candidates.length > 0 ? candidates : cards) {
1695
1733
  if (card.payment?.chain === "solana" && card.payment?.address) {
1696
- return card.payment.address;
1734
+ return card;
1697
1735
  }
1698
1736
  }
1699
1737
  return void 0;
1700
1738
  }
1739
+ function providerSolanaAddress(provider, dTag) {
1740
+ return paymentCardForCapability(provider, dTag)?.payment?.address;
1741
+ }
1742
+ function advertisedPriceForCapability(provider, dTag) {
1743
+ const card = paymentCardForCapability(provider, dTag);
1744
+ return { price: card?.payment?.job_price ?? 0, asset: assetFromCardPayment(card?.payment) };
1745
+ }
1701
1746
  function wsUrlFor(httpUrl) {
1702
1747
  return httpUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
1703
1748
  }
@@ -1747,6 +1792,29 @@ ${formatNetworkBaseline(baseline)}`;
1747
1792
  return "";
1748
1793
  }
1749
1794
  }
1795
+ async function confirmPriceGate(opts) {
1796
+ const { agent, providerLabel, capability, price, asset, maxPriceLamports, toolName } = opts;
1797
+ if (maxPriceLamports !== void 0 && price > maxPriceLamports) {
1798
+ return errorResult(
1799
+ `Price ${formatAssetAmount(asset, BigInt(price))} exceeds max ${formatAssetAmount(asset, BigInt(maxPriceLamports))}`
1800
+ );
1801
+ }
1802
+ if (price > 0 && maxPriceLamports === void 0) {
1803
+ const gasLine = await gasHintForCardAsset(agent, asset);
1804
+ const subject = toolName === "buy_capability" ? `Capability "${capability}" from "${providerLabel}"` : `Job for capability "${capability}" from "${providerLabel}"`;
1805
+ const { text } = sanitizeUntrusted(
1806
+ `${subject} costs ${formatAssetAmount(asset, BigInt(price))}.${gasLine}
1807
+
1808
+ To confirm, call ${toolName} again with max_price_lamports set (e.g. ${price} or higher).`,
1809
+ "text"
1810
+ );
1811
+ return { content: [{ type: "text", text }] };
1812
+ }
1813
+ return null;
1814
+ }
1815
+ function rejectWithProviderError(reject, providerError) {
1816
+ reject(new Error(`Job error: ${sanitizeUntrusted(providerError, "text").text}`));
1817
+ }
1750
1818
  var paymentStrategy = new SolanaPaymentStrategy();
1751
1819
  async function executePaymentFlow(agent, paymentRequest, jobId, providerPubkey, expectedRecipient) {
1752
1820
  let requestData;
@@ -1989,6 +2057,22 @@ async function executeSubmitAndPay(ctx, agent, params) {
1989
2057
  `Cannot buy from yourself - your agent's Solana wallet (${buyerWallet}) matches the provider's payment address. Use a different agent or provider.`
1990
2058
  );
1991
2059
  }
2060
+ const { price: advertisedPrice, asset: advertisedAsset } = advertisedPriceForCapability(
2061
+ provider,
2062
+ params.dTag
2063
+ );
2064
+ const priceGate = await confirmPriceGate({
2065
+ agent,
2066
+ providerLabel: sanitizeField(provider.name || params.providerNpub, 64),
2067
+ capability: params.capability,
2068
+ price: advertisedPrice,
2069
+ asset: advertisedAsset,
2070
+ maxPriceLamports: params.maxPriceLamports,
2071
+ toolName: params.toolName
2072
+ });
2073
+ if (priceGate) {
2074
+ return priceGate;
2075
+ }
1992
2076
  const submittedAt = Date.now();
1993
2077
  const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
1994
2078
  input: params.input,
@@ -2046,7 +2130,7 @@ ${sanitized.text}`);
2046
2130
  },
2047
2131
  onFeedback: payHandler.onFeedback,
2048
2132
  onError(error) {
2049
- reject(new Error(`Job error: ${sanitizeUntrusted(error, "text").text}`));
2133
+ rejectWithProviderError(reject, error);
2050
2134
  },
2051
2135
  onTimeout(timeoutMs) {
2052
2136
  reject(new JobWaitTimeoutError(timeoutMs));
@@ -2216,7 +2300,7 @@ var customerTools = [
2216
2300
  }
2217
2301
  },
2218
2302
  onError(error) {
2219
- reject(new Error(`Job error: ${error}`));
2303
+ rejectWithProviderError(reject, error);
2220
2304
  },
2221
2305
  onTimeout(timeoutMs) {
2222
2306
  reject(new JobWaitTimeoutError(timeoutMs));
@@ -2238,6 +2322,11 @@ var customerTools = [
2238
2322
  }
2239
2323
  return errorResult(`Failed to fetch result for event_id="${input.job_event_id}": ${msg}`);
2240
2324
  }
2325
+ if (providerPubkey === void 0) {
2326
+ return textResult(`${UNVERIFIED_PROVIDER_NOTICE}
2327
+
2328
+ ${result}`);
2329
+ }
2241
2330
  return textResult(result);
2242
2331
  }
2243
2332
  }),
@@ -2425,7 +2514,7 @@ ${wrapped}`);
2425
2514
  }),
2426
2515
  defineTool({
2427
2516
  name: "submit_and_pay_job",
2428
- description: 'Full customer flow: submit job -> auto-pay -> wait for result. Validates that the payment recipient matches the provider card. If payment succeeded but no result arrives within the wait window, this returns a non-error "still processing" notice with the event ID (NOT a failure) - re-poll get_job_result later (results persist on the relays; for long jobs, poll periodically, e.g. from a subagent). Handles both free and paid providers automatically. If max_price_lamports is not set and provider requests payment, the job is rejected with the price - set max_price_lamports to auto-approve payments up to that limit. COST: input is sent inline in the tool call, so a large input pays output tokens on the calling LLM. For files or git diffs, prefer submit_and_pay_job_from_file or submit_diff_review respectively.',
2517
+ description: 'Full customer flow: submit job -> auto-pay -> wait for result. Validates that the payment recipient matches the provider card. If payment succeeded but no result arrives within the wait window, this returns a non-error "still processing" notice with the event ID (NOT a failure) - re-poll get_job_result later (results persist on the relays; for long jobs, poll periodically, e.g. from a subagent). Handles both free and paid providers automatically. If max_price_lamports is not set and the capability is paid, this returns the advertised price for confirmation WITHOUT submitting a job - re-call with max_price_lamports set to approve payments up to that limit (this is a confirmation, not an error). COST: input is sent inline in the tool call, so a large input pays output tokens on the calling LLM. For files or git diffs, prefer submit_and_pay_job_from_file or submit_diff_review respectively.',
2429
2518
  schema: SubmitAndPayJobSchema,
2430
2519
  async handler(ctx, input) {
2431
2520
  ctx.toolRateLimiter.check();
@@ -2454,7 +2543,8 @@ ${wrapped}`);
2454
2543
  dTag: toDTag(input.capability),
2455
2544
  kindOffset: input.kind_offset,
2456
2545
  timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2457
- maxPriceLamports: input.max_price_lamports
2546
+ maxPriceLamports: input.max_price_lamports,
2547
+ toolName: "submit_and_pay_job"
2458
2548
  });
2459
2549
  }
2460
2550
  }),
@@ -2506,7 +2596,8 @@ ${wrapped}`);
2506
2596
  dTag: toDTag(input.capability),
2507
2597
  kindOffset: input.kind_offset,
2508
2598
  timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2509
- maxPriceLamports: input.max_price_lamports
2599
+ maxPriceLamports: input.max_price_lamports,
2600
+ toolName: "submit_and_pay_job_from_file"
2510
2601
  });
2511
2602
  }
2512
2603
  }),
@@ -2519,7 +2610,9 @@ ${wrapped}`);
2519
2610
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
2520
2611
  let diffResult;
2521
2612
  try {
2522
- diffResult = await computeGitDiff(input.repo_path, input.base);
2613
+ diffResult = await computeGitDiff(input.repo_path, input.base, {
2614
+ allowOutsideCwd: input.allow_outside_cwd
2615
+ });
2523
2616
  } catch (e) {
2524
2617
  return errorResult(e instanceof Error ? e.message : String(e));
2525
2618
  }
@@ -2548,7 +2641,8 @@ ${diffResult.diff}`;
2548
2641
  dTag: toDTag(input.capability),
2549
2642
  kindOffset: input.kind_offset,
2550
2643
  timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
2551
- maxPriceLamports: input.max_price_lamports
2644
+ maxPriceLamports: input.max_price_lamports,
2645
+ toolName: "submit_diff_review"
2552
2646
  });
2553
2647
  }
2554
2648
  }),
@@ -2590,30 +2684,17 @@ ${diffResult.diff}`;
2590
2684
  );
2591
2685
  return errorResult(text);
2592
2686
  }
2593
- const price = card.payment?.job_price ?? 0;
2594
- const cardAsset = assetFromCardPayment(card.payment);
2595
- if (input.max_price_lamports !== void 0 && price > input.max_price_lamports) {
2596
- return errorResult(
2597
- `Price ${formatAssetAmount(cardAsset, BigInt(price))} exceeds max ${formatAssetAmount(cardAsset, BigInt(input.max_price_lamports))}`
2598
- );
2599
- }
2600
- if (price > 0 && input.max_price_lamports === void 0) {
2601
- const gasLine = await gasHintForCardAsset(agent, cardAsset);
2602
- const safeProviderName = sanitizeField(provider.name || input.provider_npub, 64);
2603
- const { text } = sanitizeUntrusted(
2604
- `Capability "${input.capability}" from "${safeProviderName}" costs ${formatAssetAmount(cardAsset, BigInt(price))}.${gasLine}
2605
-
2606
- To confirm, call buy_capability again with max_price_lamports set (e.g. ${price} or higher).`,
2607
- "text"
2608
- );
2609
- return {
2610
- content: [
2611
- {
2612
- type: "text",
2613
- text
2614
- }
2615
- ]
2616
- };
2687
+ const priceGate = await confirmPriceGate({
2688
+ agent,
2689
+ providerLabel: sanitizeField(provider.name || input.provider_npub, 64),
2690
+ capability: input.capability,
2691
+ price: card.payment?.job_price ?? 0,
2692
+ asset: assetFromCardPayment(card.payment),
2693
+ maxPriceLamports: input.max_price_lamports,
2694
+ toolName: "buy_capability"
2695
+ });
2696
+ if (priceGate) {
2697
+ return priceGate;
2617
2698
  }
2618
2699
  const expectedRecipient = card.payment?.chain === "solana" ? card.payment.address : void 0;
2619
2700
  if (agent.solanaKeypair && !expectedRecipient) {
@@ -2682,7 +2763,7 @@ ${sanitized.text}`
2682
2763
  },
2683
2764
  onFeedback: payHandler.onFeedback,
2684
2765
  onError(error) {
2685
- reject(new Error(`Job error: ${error}`));
2766
+ rejectWithProviderError(reject, error);
2686
2767
  },
2687
2768
  onTimeout(timeoutMs) {
2688
2769
  reject(new JobWaitTimeoutError(timeoutMs));