@elisym/mcp 0.25.0 → 0.25.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,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { LIMITS, SESSION_ID_REGEX, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, exportKeyPairBytes, RELAYS, toDTag, resolveUsdcAsset, deriveOwnerDelegationAta, getDelegation, formatAssetAmount, mintDelegationNonce, buildDelegationAuthProof, JobWaitTimeoutError, assetKey, MAX_PROOF_TTL_SECS, attachmentsOf, decodeJobPayload, utf8ByteLength, estimateNetworkBaseline, formatSol as formatSol$1, resolveKnownAsset, verifyAgentIdentities, splAssetsForNetwork, parseAssetAmount, delegationApproveFeeSubunits, buildApproveDelegate, decodeApproveDelegate, decodeDelegationFeeTransfer, buildRevokeDelegate, estimateSolFeeLamports, formatFeeBreakdown, getProtocolProgramId, isDefinitelyUnpaid, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, resolveLsmAsset, ElisymIdentity, ElisymClient, encodeJobPayload, NATIVE_SOL, assetByKey, sendConfirmToTerminal, ONCHAIN_DISCLAIMER, defaultCeilings, verifyOnchainCall, ONCHAIN_REFUSAL_HEADLINES, getProtocolConfig, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT_BASE, USDC_SOLANA_DEVNET, USDC_SOLANA_MAINNET, LSM_SOLANA_MAINNET, estimateAssetStatsRentLamports, formatNetworkBaseline, MAX_CALL_BASE64_CHARS, MAX_EXPLAIN_ENTRIES, MAX_EXPLAIN_TEXT_CHARS, ONCHAIN_UNATTRIBUTED_NOTICE, KNOWN_ASSETS } from '@elisym/sdk';
3
- import { listAgents, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, resolveAgent, loadAgent, globalConfigPath, writeYaml, ensureGitignoreHasMessagesEntry, writeFileAtomic as writeFileAtomic$1, ensureGitignoreHasJobSessionsEntry } from '@elisym/sdk/agent-store';
2
+ import { LIMITS, SESSION_ID_REGEX, DEFAULT_KIND_OFFSET, SolanaPaymentStrategy, makeCensor, DEFAULT_REDACT_PATHS, validateAgentName, exportKeyPairBytes, RELAYS, toDTag, utf8ByteLength, JobWaitTimeoutError, attachmentsOf, decodeJobPayload, formatAssetAmount, estimateNetworkBaseline, formatSol as formatSol$1, resolveKnownAsset, verifyAgentIdentities, splAssetsForNetwork, deriveOwnerDelegationAta, getDelegation, resolveUsdcAsset, parseAssetAmount, delegationApproveFeeSubunits, buildApproveDelegate, decodeApproveDelegate, decodeDelegationFeeTransfer, buildRevokeDelegate, estimateSolFeeLamports, formatFeeBreakdown, getProtocolProgramId, isDefinitelyUnpaid, resolveAssetFromPaymentRequest as resolveAssetFromPaymentRequest$1, resolveLsmAsset, ElisymIdentity, ElisymClient, mintDelegationNonce, buildDelegationAuthProof, assetKey, MAX_PROOF_TTL_SECS, encodeJobPayload, NATIVE_SOL, assetByKey, sendConfirmToTerminal, ONCHAIN_DISCLAIMER, defaultCeilings, verifyOnchainCall, ONCHAIN_REFUSAL_HEADLINES, getProtocolConfig, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT_BASE, USDC_SOLANA_DEVNET, USDC_SOLANA_MAINNET, LSM_SOLANA_MAINNET, estimateAssetStatsRentLamports, formatNetworkBaseline, MAX_CALL_BASE64_CHARS, MAX_EXPLAIN_ENTRIES, MAX_EXPLAIN_TEXT_CHARS, ONCHAIN_UNATTRIBUTED_NOTICE, KNOWN_ASSETS } from '@elisym/sdk';
3
+ import { listAgents, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, resolveAgent, loadAgent, globalConfigPath, writeYaml, writeFileAtomic as writeFileAtomic$1, ensureGitignoreHasMessagesEntry, ensureGitignoreHasJobSessionsEntry } from '@elisym/sdk/agent-store';
4
4
  import { createIrohTransport, loadGlobalConfig, writeGlobalConfig } from '@elisym/sdk/node';
5
- import { getBase58Encoder, getBase58Decoder, generateKeyPairSigner, createSolanaRpc, createKeyPairSignerFromBytes, address, getSignatureFromTransaction, createSolanaRpcSubscriptions, sendAndConfirmTransactionFactory, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, getBase64Encoder, getTransactionDecoder, signTransaction, isAddress } from '@solana/kit';
5
+ import { getBase58Encoder, getBase58Decoder, generateKeyPairSigner, createSolanaRpc, address, getSignatureFromTransaction, createSolanaRpcSubscriptions, sendAndConfirmTransactionFactory, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, createKeyPairSignerFromBytes, getBase64Encoder, getTransactionDecoder, signTransaction, isAddress } from '@solana/kit';
6
6
  import { Command } from 'commander';
7
7
  import { generateSecretKey, nip19, getPublicKey, verifyEvent } from 'nostr-tools';
8
8
  import { writeFile, realpath, readFile, rm, stat, mkdir, rename, unlink } from 'node:fs/promises';
@@ -2519,6 +2519,11 @@ var SubmitAndPayJobFromFileSchema = z.object({
2519
2519
  ),
2520
2520
  session_id: SessionIdSchema
2521
2521
  });
2522
+ var SubmitDelegatedJobFromFileSchema = SubmitAndPayJobFromFileSchema.omit({
2523
+ session_id: true
2524
+ }).extend({
2525
+ max_price_lamports: SubmitDelegatedJobSchema.shape.max_price_lamports
2526
+ });
2522
2527
  var SubmitDiffReviewSchema = z.object({
2523
2528
  provider_npub: z.string(),
2524
2529
  capability: z.string().min(1).max(64).default("review").describe('Capability tag advertised by the reviewer. Override if not "review".'),
@@ -2699,6 +2704,9 @@ ${formatNetworkBaseline(baseline)}${statsHint}`;
2699
2704
  return "";
2700
2705
  }
2701
2706
  }
2707
+ function isDelegatedTool(toolName) {
2708
+ return toolName === "submit_delegated_job" || toolName === "submit_delegated_job_from_file";
2709
+ }
2702
2710
  async function confirmPriceGate(opts) {
2703
2711
  const {
2704
2712
  agent,
@@ -2720,7 +2728,7 @@ async function confirmPriceGate(opts) {
2720
2728
  if (price > 0 && maxPriceLamports === void 0) {
2721
2729
  const gasLine = await gasHintForCardAsset(agent, asset);
2722
2730
  const subject = toolName === "buy_capability" ? `Capability "${capability}" from "${providerLabel}"` : `Job for capability "${capability}" from "${providerLabel}"`;
2723
- const costLine = meteredMinSubunits !== void 0 && toolName === "submit_delegated_job" ? `${subject} is billed for what it actually uses: from ${formatAssetAmount(asset, meteredMinSubunits)} and never more than ${formatAssetAmount(asset, BigInt(price))}.` : `${subject} costs ${formatAssetAmount(asset, BigInt(price))}.`;
2731
+ const costLine = meteredMinSubunits !== void 0 && isDelegatedTool(toolName) ? `${subject} is billed for what it actually uses: from ${formatAssetAmount(asset, meteredMinSubunits)} and never more than ${formatAssetAmount(asset, BigInt(price))}.` : `${subject} costs ${formatAssetAmount(asset, BigInt(price))}.`;
2724
2732
  const { text } = sanitizeUntrusted(
2725
2733
  `${costLine}${gasLine}
2726
2734
 
@@ -2997,6 +3005,51 @@ async function prepareTextInput(agent, text, options) {
2997
3005
  };
2998
3006
  }
2999
3007
  }
3008
+ var FILE_PROMPT_INLINE_CAP = 55e3;
3009
+ async function validateFileInput(ctx, options) {
3010
+ const prompt = options.prompt.trim();
3011
+ if (utf8ByteLength(prompt) > FILE_PROMPT_INLINE_CAP) {
3012
+ return {
3013
+ error: `Prompt is too long to ride inline (max ${FILE_PROMPT_INLINE_CAP} bytes); the file occupies the only attachment slot, so shorten the prompt.`
3014
+ };
3015
+ }
3016
+ let file;
3017
+ try {
3018
+ file = await prepareFileInput(options.inputPath, {
3019
+ allowOutsideCwd: options.allowOutsideCwd
3020
+ });
3021
+ } catch (e) {
3022
+ return { error: e instanceof Error ? e.message : String(e) };
3023
+ }
3024
+ const agent = ctx.active();
3025
+ if (agent.agentDir === void 0) {
3026
+ return {
3027
+ error: `Sending a file requires a persistent agent (this is an ephemeral session). Files are always transferred P2P via iroh, never inline.`
3028
+ };
3029
+ }
3030
+ return { agent, prompt, file };
3031
+ }
3032
+ async function seedFileAttachment(agent, file) {
3033
+ try {
3034
+ const seeded = await ensureIrohTransport(agent).seedPath(file.absPath);
3035
+ return {
3036
+ attachment: {
3037
+ name: file.name,
3038
+ size: seeded.size,
3039
+ mime: file.mime,
3040
+ transports: [{ kind: "iroh", ticket: seeded.ticket }]
3041
+ }
3042
+ };
3043
+ } catch (e) {
3044
+ const msg = e instanceof Error ? e.message : String(e);
3045
+ if (/@number0\/iroh|iroh file transfer is unavailable/i.test(msg)) {
3046
+ return {
3047
+ error: `File transfer is unavailable: the optional @number0/iroh addon is not installed. Install it (e.g. \`bun add @number0/iroh\`) to send files.`
3048
+ };
3049
+ }
3050
+ return { error: `Failed to seed file for transfer: ${msg}` };
3051
+ }
3052
+ }
3000
3053
  async function executeSubmitAndPay(ctx, agent, params) {
3001
3054
  const ping = await agent.client.ping.pingAgent(params.providerPubkey, PRE_PING_TIMEOUT_MS);
3002
3055
  if (!ping.online) {
@@ -3225,6 +3278,234 @@ ${sessionIdLine(sessionId).trimEnd()}` : "";
3225
3278
  );
3226
3279
  }
3227
3280
  }
3281
+ async function executeDelegatedJob(ctx, agent, params) {
3282
+ if (!agent.solanaKeypair) {
3283
+ return errorResult(
3284
+ "Solana wallet not configured for this agent - delegated payment signs the proof with the owner (wallet) key."
3285
+ );
3286
+ }
3287
+ const providerPubkey = decodeNpub(params.providerNpub);
3288
+ const dTag = toDTag(params.capability);
3289
+ const { timeoutMs } = params;
3290
+ const ping = await agent.client.ping.pingAgent(providerPubkey, PRE_PING_TIMEOUT_MS);
3291
+ if (!ping.online) {
3292
+ return errorResult(
3293
+ `Provider ${params.providerNpub} is offline. Run search_agents to find currently-online providers.`
3294
+ );
3295
+ }
3296
+ const providers = await agent.client.discovery.fetchAgents(agent.network);
3297
+ const provider = providers.find((candidate) => candidate.npub === params.providerNpub);
3298
+ if (!provider) {
3299
+ return errorResult(
3300
+ `Provider ${params.providerNpub} not found on ${agent.network}. Refresh discovery (e.g. search_agents) or verify the npub is correct.`
3301
+ );
3302
+ }
3303
+ const card = paymentCardForCapability(provider, dTag);
3304
+ const descriptor = card?.delegation;
3305
+ if (!descriptor) {
3306
+ const payUpFrontTool = params.toolName === "submit_delegated_job_from_file" ? "submit_and_pay_job_from_file" : "submit_and_pay_job";
3307
+ return errorResult(
3308
+ `Capability "${params.capability}" of ${params.providerNpub} does not advertise delegated payment. Use ${payUpFrontTool} instead.`
3309
+ );
3310
+ }
3311
+ const { price, asset } = advertisedPriceForCapability(provider, dTag);
3312
+ if (price <= 0) {
3313
+ return errorResult(
3314
+ `Capability "${params.capability}" advertises no price - delegated payment needs a priced skill. Use create_job for free capabilities.`
3315
+ );
3316
+ }
3317
+ const delegationAsset = resolveUsdcAsset(agent.network);
3318
+ const cardPayment = paymentCardForCapability(provider, dTag)?.payment;
3319
+ const cardMint = cardPayment?.mint;
3320
+ const isCanonicalUsdc = (cardPayment?.chain ?? delegationAsset.chain) === delegationAsset.chain && cardPayment?.token?.toLowerCase() === delegationAsset.token && (cardMint === void 0 || cardMint === delegationAsset.mint);
3321
+ if (!isCanonicalUsdc) {
3322
+ const declared = cardPayment?.symbol ?? cardPayment?.token ?? asset.symbol;
3323
+ const { text } = sanitizeUntrusted(
3324
+ `Delegated payment is USDC-only, but this capability is priced in ${declared}.`,
3325
+ "text"
3326
+ );
3327
+ return errorResult(text);
3328
+ }
3329
+ const priceSubunits = BigInt(price);
3330
+ const rpc = createSolanaRpc(rpcUrlFor(agent.network));
3331
+ const ownerAta = await deriveOwnerDelegationAta(agent.solanaKeypair.publicKey, agent.network);
3332
+ let delegation;
3333
+ try {
3334
+ delegation = await getDelegation(rpc, ownerAta);
3335
+ } catch (e) {
3336
+ const message = e instanceof Error ? e.message : String(e);
3337
+ return errorResult(`Could not read your delegation on-chain: ${message}`);
3338
+ }
3339
+ if (delegation === null || delegation.delegate === null) {
3340
+ return errorResult(
3341
+ "No active delegation from this wallet. Approve one first (web app Delegation tab, or the approve flow), then retry."
3342
+ );
3343
+ }
3344
+ if (delegation.delegate !== descriptor.delegate_pubkey) {
3345
+ const { text } = sanitizeUntrusted(
3346
+ `Your delegation is granted to ${delegation.delegate}, but this capability advertises delegate ${descriptor.delegate_pubkey} (the provider may have rotated its key). Re-approve the delegation, then retry.`,
3347
+ "text"
3348
+ );
3349
+ return errorResult(text);
3350
+ }
3351
+ if (delegation.remainingCap < priceSubunits) {
3352
+ return errorResult(
3353
+ `Remaining delegated cap ${formatAssetAmount(delegationAsset, delegation.remainingCap)} is below the price ${formatAssetAmount(delegationAsset, priceSubunits)}. Top up the delegation (re-approve) first.`
3354
+ );
3355
+ }
3356
+ if (delegation.balance < priceSubunits) {
3357
+ return errorResult(
3358
+ `Delegation account balance ${formatAssetAmount(delegationAsset, delegation.balance)} is below the price ${formatAssetAmount(delegationAsset, priceSubunits)}. Fund the wallet first.`
3359
+ );
3360
+ }
3361
+ try {
3362
+ assertCanSpend(ctx, delegationAsset, priceSubunits);
3363
+ } catch (e) {
3364
+ return errorResult(e instanceof Error ? e.message : String(e));
3365
+ }
3366
+ const priceGate = await confirmPriceGate({
3367
+ agent,
3368
+ providerLabel: sanitizeField(provider.name || params.providerNpub, 64),
3369
+ capability: params.capability,
3370
+ price,
3371
+ asset: delegationAsset,
3372
+ maxPriceLamports: params.maxPriceLamports,
3373
+ toolName: params.toolName,
3374
+ // Card-published floor, so the quote reads "from X, never more than Y"
3375
+ // instead of a flat number the buyer will usually not be charged.
3376
+ ...card?.metered !== void 0 ? { meteredMinSubunits: BigInt(card.metered.min_subunits) } : {}
3377
+ });
3378
+ if (priceGate) {
3379
+ return priceGate;
3380
+ }
3381
+ const preparedInput = await params.prepareInput();
3382
+ if ("error" in preparedInput) {
3383
+ return errorResult(preparedInput.error);
3384
+ }
3385
+ const ownerSigner = await createKeyPairSignerFromBytes(agent.solanaKeypair.secretKey);
3386
+ const expiryUnix = Math.floor(Date.now() / 1e3) + MAX_PROOF_TTL_SECS;
3387
+ const nonce = mintDelegationNonce();
3388
+ const proof = await buildDelegationAuthProof({
3389
+ ownerSigner,
3390
+ agentDelegate: descriptor.delegate_pubkey,
3391
+ nostrAuthor: agent.identity.publicKey,
3392
+ owner: agent.solanaKeypair.publicKey,
3393
+ expiryUnix,
3394
+ nonce
3395
+ });
3396
+ const submittedAt = Date.now();
3397
+ const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
3398
+ input: preparedInput.input,
3399
+ attachment: preparedInput.attachment,
3400
+ capability: dTag,
3401
+ providerPubkey,
3402
+ kindOffset: params.kindOffset,
3403
+ acceptTransports: MCP_ACCEPT_TRANSPORTS,
3404
+ delegatedPayment: {
3405
+ owner: agent.solanaKeypair.publicKey,
3406
+ expiryUnix,
3407
+ nonce,
3408
+ proof
3409
+ }
3410
+ });
3411
+ let resultAttachment;
3412
+ let pullTx;
3413
+ let settledAmountSubunits;
3414
+ try {
3415
+ const result = await awaitJobResult(
3416
+ agent,
3417
+ {},
3418
+ ({ resolve, reject }) => ({
3419
+ jobEventId: jobId,
3420
+ providerPubkey,
3421
+ customerPublicKey: agent.identity.publicKey,
3422
+ callbacks: {
3423
+ onResult(content, _eventId, attachment, _attachments, paymentTx, paidAmountSubunits) {
3424
+ pullTx = paymentTx;
3425
+ settledAmountSubunits = paidAmountSubunits;
3426
+ if (attachment) {
3427
+ resultAttachment = attachment;
3428
+ resolve(formatFileResultMetadata(jobId, attachment));
3429
+ return;
3430
+ }
3431
+ const sanitized = sanitizeResultContent(content);
3432
+ resolve(`Job completed.
3433
+
3434
+ ${sanitized.text}`);
3435
+ },
3436
+ onError(error) {
3437
+ rejectWithProviderError(reject, error);
3438
+ },
3439
+ onTimeout(waitedMs) {
3440
+ reject(new JobWaitTimeoutError(waitedMs));
3441
+ }
3442
+ },
3443
+ timeoutMs,
3444
+ customerSecretKey: agent.identity.secretKey
3445
+ }),
3446
+ timeoutMs + 5e3
3447
+ );
3448
+ const settledForSession = settledSubunitsForHistory(card, price, settledAmountSubunits);
3449
+ recordSpend(ctx, delegationAsset, BigInt(settledForSession));
3450
+ await recordJobOutcome(agent, {
3451
+ jobEventId: jobId,
3452
+ capability: dTag,
3453
+ providerPubkey,
3454
+ providerName: clipProviderName(provider.name),
3455
+ // What actually moved, as recorded in the buyer's OWN history.
3456
+ //
3457
+ // On a FLAT card the runtime always pulls exactly `price`, so there is
3458
+ // nothing to learn from the provider and the reported figure is only a
3459
+ // way to lie - the card price is the truth by construction.
3460
+ //
3461
+ // On a METERED card the pull is variable, so the provider's report is
3462
+ // the only source of the real number. It is still attacker-controlled
3463
+ // and arrives through a tag parser that accepts negatives and
3464
+ // prefix-parses garbage, so it is trusted only inside the range the
3465
+ // card published and the buyer approved: `[min, price]`. Anything
3466
+ // outside degrades to the ceiling rather than to the attacker's value.
3467
+ paidAmountSubunits: String(settledForSession),
3468
+ // The registry key, not the card's. A mint-less `usdc` card is
3469
+ // accepted by the gate above but keys as `solana:usdc`, which
3470
+ // `assetByKey` cannot resolve and which splits any per-asset
3471
+ // aggregation away from the canonical `solana:usdc:<mint>` rows.
3472
+ assetKey: assetKey(delegationAsset),
3473
+ status: "completed",
3474
+ submittedAt,
3475
+ completedAt: Date.now(),
3476
+ resultPreview: result.slice(0, RESULT_PREVIEW_MAX_LEN),
3477
+ paymentSig: pullTx,
3478
+ attachmentJson: resultAttachment ? JSON.stringify(resultAttachment) : void 0
3479
+ });
3480
+ const pullLine = pullTx !== void 0 ? `pull_tx=${pullTx}
3481
+ ` : "";
3482
+ const tip = buildJobCompletionTip(jobId, params.providerNpub);
3483
+ return textResult(`event_id=${jobId}
3484
+ ${pullLine}${result}${tip}`);
3485
+ } catch (e) {
3486
+ const msg = e instanceof Error ? e.message : String(e);
3487
+ const isTimeout = e instanceof JobWaitTimeoutError;
3488
+ await recordJobOutcome(agent, {
3489
+ jobEventId: jobId,
3490
+ capability: dTag,
3491
+ providerPubkey,
3492
+ providerName: clipProviderName(provider.name),
3493
+ status: isTimeout ? "timeout" : "failed",
3494
+ submittedAt,
3495
+ completedAt: Date.now()
3496
+ });
3497
+ if (isTimeout) {
3498
+ return textResult(
3499
+ `event_id=${jobId}
3500
+ Still processing (delegated: the provider pulls payment only after delivering the result). This is NOT an error - retry get_job_result with event_id="${jobId}" later. If the provider never starts, the proof expires within ${MAX_PROOF_TTL_SECS / 60} minutes with no charge.`
3501
+ );
3502
+ }
3503
+ const safeMsg = sanitizeUntrusted(msg, "text").text;
3504
+ return errorResult(
3505
+ `Job ${jobId} failed: ${safeMsg}. Delegated jobs charge only on delivery - no charge occurred unless a pull was reported.`
3506
+ );
3507
+ }
3508
+ }
3228
3509
  function awaitJobResult(agent, options, fn, safetyTimeoutMs) {
3229
3510
  return new Promise((resolve, reject) => {
3230
3511
  let closeFn = null;
@@ -3333,233 +3614,59 @@ var customerTools = [
3333
3614
  }),
3334
3615
  defineTool({
3335
3616
  name: "submit_delegated_job",
3336
- description: "Submit a job paid from your existing spl-approve USDC delegation: the provider does the work FIRST, then pulls from your delegated allowance - no per-job payment transaction from you. On an ordinary capability it pulls the advertised price. On a METERED one (the card carries a `metered` block) the advertised price is a CEILING and the pull is what the job actually consumed, never more than that ceiling - so a metered card is usually cheaper here than its listed price suggests. Requires an ACTIVE delegation to the delegate key this capability advertises (check with get_delegation). Within the approved cap the delegate can pull without your signature, so treat the cap as the max loss. Your per-session spend limit also applies: the job is refused if its ceiling does not fit the remaining session budget. If max_price_lamports is not set, returns the price - or the range, when metered - for confirmation without publishing anything.",
3617
+ description: "Submit a job paid from your existing spl-approve USDC delegation: the provider does the work FIRST, then pulls from your delegated allowance - no per-job payment transaction from you. On an ordinary capability it pulls the advertised price. On a METERED one (the card carries a `metered` block) the advertised price is a CEILING and the pull is what the job actually consumed, never more than that ceiling - so a metered card is usually cheaper here than its listed price suggests. Requires an ACTIVE delegation to the delegate key this capability advertises (check with get_delegation). Within the approved cap the delegate can pull without your signature, so treat the cap as the max loss. Your per-session spend limit also applies: the job is refused if its ceiling does not fit the remaining session budget. An input too large to ride inline is sent via iroh automatically (needs a persistent agent); for a file on disk use submit_delegated_job_from_file. If max_price_lamports is not set, returns the price - or the range, when metered - for confirmation without publishing anything.",
3337
3618
  schema: SubmitDelegatedJobSchema,
3338
3619
  async handler(ctx, input) {
3339
3620
  ctx.toolRateLimiter.check();
3340
- checkLen("input", input.input, MAX_INPUT_LEN);
3341
3621
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
3342
- const agent = ctx.active();
3343
- if (!agent.solanaKeypair) {
3344
- return errorResult(
3345
- "Solana wallet not configured for this agent - delegated payment signs the proof with the owner (wallet) key."
3346
- );
3347
- }
3348
- const providerPubkey = decodeNpub(input.provider_npub);
3349
- const dTag = toDTag(input.capability);
3350
- const timeoutMs = Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3;
3351
- const ping = await agent.client.ping.pingAgent(providerPubkey, PRE_PING_TIMEOUT_MS);
3352
- if (!ping.online) {
3353
- return errorResult(
3354
- `Provider ${input.provider_npub} is offline. Run search_agents to find currently-online providers.`
3355
- );
3356
- }
3357
- const providers = await agent.client.discovery.fetchAgents(agent.network);
3358
- const provider = providers.find((candidate) => candidate.npub === input.provider_npub);
3359
- if (!provider) {
3360
- return errorResult(
3361
- `Provider ${input.provider_npub} not found on ${agent.network}. Refresh discovery (e.g. search_agents) or verify the npub is correct.`
3362
- );
3363
- }
3364
- const card = paymentCardForCapability(provider, dTag);
3365
- const descriptor = card?.delegation;
3366
- if (!descriptor) {
3367
- return errorResult(
3368
- `Capability "${input.capability}" of ${input.provider_npub} does not advertise delegated payment. Use submit_and_pay_job instead.`
3369
- );
3370
- }
3371
- const { price, asset } = advertisedPriceForCapability(provider, dTag);
3372
- if (price <= 0) {
3373
- return errorResult(
3374
- `Capability "${input.capability}" advertises no price - delegated payment needs a priced skill. Use create_job for free capabilities.`
3375
- );
3376
- }
3377
- const delegationAsset = resolveUsdcAsset(agent.network);
3378
- const cardPayment = paymentCardForCapability(provider, dTag)?.payment;
3379
- const cardMint = cardPayment?.mint;
3380
- const isCanonicalUsdc = (cardPayment?.chain ?? delegationAsset.chain) === delegationAsset.chain && cardPayment?.token?.toLowerCase() === delegationAsset.token && (cardMint === void 0 || cardMint === delegationAsset.mint);
3381
- if (!isCanonicalUsdc) {
3382
- const declared = cardPayment?.symbol ?? cardPayment?.token ?? asset.symbol;
3383
- const { text } = sanitizeUntrusted(
3384
- `Delegated payment is USDC-only, but this capability is priced in ${declared}.`,
3385
- "text"
3386
- );
3387
- return errorResult(text);
3388
- }
3389
- const priceSubunits = BigInt(price);
3390
- const rpc = createSolanaRpc(rpcUrlFor(agent.network));
3391
- const ownerAta = await deriveOwnerDelegationAta(agent.solanaKeypair.publicKey, agent.network);
3392
- let delegation;
3393
- try {
3394
- delegation = await getDelegation(rpc, ownerAta);
3395
- } catch (e) {
3396
- const message = e instanceof Error ? e.message : String(e);
3397
- return errorResult(`Could not read your delegation on-chain: ${message}`);
3398
- }
3399
- if (delegation === null || delegation.delegate === null) {
3400
- return errorResult(
3401
- "No active delegation from this wallet. Approve one first (web app Delegation tab, or the approve flow), then retry."
3402
- );
3403
- }
3404
- if (delegation.delegate !== descriptor.delegate_pubkey) {
3405
- const { text } = sanitizeUntrusted(
3406
- `Your delegation is granted to ${delegation.delegate}, but this capability advertises delegate ${descriptor.delegate_pubkey} (the provider may have rotated its key). Re-approve the delegation, then retry.`,
3407
- "text"
3408
- );
3409
- return errorResult(text);
3410
- }
3411
- if (delegation.remainingCap < priceSubunits) {
3412
- return errorResult(
3413
- `Remaining delegated cap ${formatAssetAmount(delegationAsset, delegation.remainingCap)} is below the price ${formatAssetAmount(delegationAsset, priceSubunits)}. Top up the delegation (re-approve) first.`
3414
- );
3415
- }
3416
- if (delegation.balance < priceSubunits) {
3622
+ const inputBytes = utf8ByteLength(input.input);
3623
+ if (inputBytes > LIMITS.MAX_REINLINE_TEXT_BYTES) {
3417
3624
  return errorResult(
3418
- `Delegation account balance ${formatAssetAmount(delegationAsset, delegation.balance)} is below the price ${formatAssetAmount(delegationAsset, priceSubunits)}. Fund the wallet first.`
3625
+ `Input is ${inputBytes} bytes (max ${LIMITS.MAX_REINLINE_TEXT_BYTES} for an inline job). Send a large file with submit_delegated_job_from_file.`
3419
3626
  );
3420
3627
  }
3421
- try {
3422
- assertCanSpend(ctx, delegationAsset, priceSubunits);
3423
- } catch (e) {
3424
- return errorResult(e instanceof Error ? e.message : String(e));
3425
- }
3426
- const priceGate = await confirmPriceGate({
3427
- agent,
3428
- providerLabel: sanitizeField(provider.name || input.provider_npub, 64),
3628
+ const agent = ctx.active();
3629
+ return executeDelegatedJob(ctx, agent, {
3630
+ // Large input spills to iroh transparently; small input stays inline.
3631
+ prepareInput: () => prepareTextInput(agent, input.input),
3632
+ providerNpub: input.provider_npub,
3429
3633
  capability: input.capability,
3430
- price,
3431
- asset: delegationAsset,
3634
+ kindOffset: input.kind_offset,
3635
+ timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
3432
3636
  maxPriceLamports: input.max_price_lamports,
3433
- toolName: "submit_delegated_job",
3434
- // Card-published floor, so the quote reads "from X, never more than Y"
3435
- // instead of a flat number the buyer will usually not be charged.
3436
- ...card?.metered !== void 0 ? { meteredMinSubunits: BigInt(card.metered.min_subunits) } : {}
3637
+ toolName: "submit_delegated_job"
3437
3638
  });
3438
- if (priceGate) {
3439
- return priceGate;
3440
- }
3441
- const ownerSigner = await createKeyPairSignerFromBytes(agent.solanaKeypair.secretKey);
3442
- const expiryUnix = Math.floor(Date.now() / 1e3) + MAX_PROOF_TTL_SECS;
3443
- const nonce = mintDelegationNonce();
3444
- const proof = await buildDelegationAuthProof({
3445
- ownerSigner,
3446
- agentDelegate: descriptor.delegate_pubkey,
3447
- nostrAuthor: agent.identity.publicKey,
3448
- owner: agent.solanaKeypair.publicKey,
3449
- expiryUnix,
3450
- nonce
3639
+ }
3640
+ }),
3641
+ defineTool({
3642
+ name: "submit_delegated_job_from_file",
3643
+ description: "Same as submit_delegated_job, but the job input is read from a file on disk by the MCP server and sent peer-to-peer via iroh - the file content never enters the model's output tokens. Prefer this over submit_and_pay_job_from_file whenever the capability advertises delegation: that tool pays the full listed price up front, while here the provider pulls from your delegation after delivering - on a METERED card only what the job consumed, never more than the listed price. Requires an ACTIVE delegation to the delegate key the capability advertises (check with get_delegation), a persistent agent, and the iroh addon. Text files reach the skill on stdin; binary files via ELISYM_INPUT_FILE. Pass an optional `prompt` to send a text instruction alongside the file; it rides inline (encrypted) while the file rides P2P. If max_price_lamports is not set, returns the price - or the range, when metered - for confirmation without publishing anything.",
3644
+ schema: SubmitDelegatedJobFromFileSchema,
3645
+ async handler(ctx, input) {
3646
+ ctx.toolRateLimiter.check();
3647
+ checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
3648
+ const validated = await validateFileInput(ctx, {
3649
+ inputPath: input.input_path,
3650
+ allowOutsideCwd: input.allow_outside_cwd,
3651
+ prompt: input.prompt
3451
3652
  });
3452
- const submittedAt = Date.now();
3453
- const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
3454
- input: input.input,
3455
- capability: dTag,
3456
- providerPubkey,
3653
+ if ("error" in validated) {
3654
+ return errorResult(validated.error);
3655
+ }
3656
+ const { agent, prompt, file } = validated;
3657
+ return executeDelegatedJob(ctx, agent, {
3658
+ // Seeded lazily: the delegation pre-checks and price gate run first.
3659
+ prepareInput: async () => {
3660
+ const seeded = await seedFileAttachment(agent, file);
3661
+ return "error" in seeded ? seeded : { input: prompt, attachment: seeded.attachment };
3662
+ },
3663
+ providerNpub: input.provider_npub,
3664
+ capability: input.capability,
3457
3665
  kindOffset: input.kind_offset,
3458
- acceptTransports: MCP_ACCEPT_TRANSPORTS,
3459
- delegatedPayment: {
3460
- owner: agent.solanaKeypair.publicKey,
3461
- expiryUnix,
3462
- nonce,
3463
- proof
3464
- }
3666
+ timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
3667
+ maxPriceLamports: input.max_price_lamports,
3668
+ toolName: "submit_delegated_job_from_file"
3465
3669
  });
3466
- let resultAttachment;
3467
- let pullTx;
3468
- let settledAmountSubunits;
3469
- try {
3470
- const result = await awaitJobResult(
3471
- agent,
3472
- {},
3473
- ({ resolve, reject }) => ({
3474
- jobEventId: jobId,
3475
- providerPubkey,
3476
- customerPublicKey: agent.identity.publicKey,
3477
- callbacks: {
3478
- onResult(content, _eventId, attachment, _attachments, paymentTx, paidAmountSubunits) {
3479
- pullTx = paymentTx;
3480
- settledAmountSubunits = paidAmountSubunits;
3481
- if (attachment) {
3482
- resultAttachment = attachment;
3483
- resolve(formatFileResultMetadata(jobId, attachment));
3484
- return;
3485
- }
3486
- const sanitized = sanitizeResultContent(content);
3487
- resolve(`Job completed.
3488
-
3489
- ${sanitized.text}`);
3490
- },
3491
- onError(error) {
3492
- rejectWithProviderError(reject, error);
3493
- },
3494
- onTimeout(waitedMs) {
3495
- reject(new JobWaitTimeoutError(waitedMs));
3496
- }
3497
- },
3498
- timeoutMs,
3499
- customerSecretKey: agent.identity.secretKey
3500
- }),
3501
- timeoutMs + 5e3
3502
- );
3503
- const settledForSession = settledSubunitsForHistory(card, price, settledAmountSubunits);
3504
- recordSpend(ctx, delegationAsset, BigInt(settledForSession));
3505
- await recordJobOutcome(agent, {
3506
- jobEventId: jobId,
3507
- capability: dTag,
3508
- providerPubkey,
3509
- providerName: clipProviderName(provider.name),
3510
- // What actually moved, as recorded in the buyer's OWN history.
3511
- //
3512
- // On a FLAT card the runtime always pulls exactly `price`, so there is
3513
- // nothing to learn from the provider and the reported figure is only a
3514
- // way to lie - the card price is the truth by construction.
3515
- //
3516
- // On a METERED card the pull is variable, so the provider's report is
3517
- // the only source of the real number. It is still attacker-controlled
3518
- // and arrives through a tag parser that accepts negatives and
3519
- // prefix-parses garbage, so it is trusted only inside the range the
3520
- // card published and the buyer approved: `[min, price]`. Anything
3521
- // outside degrades to the ceiling rather than to the attacker's value.
3522
- paidAmountSubunits: String(settledForSession),
3523
- // The registry key, not the card's. A mint-less `usdc` card is
3524
- // accepted by the gate above but keys as `solana:usdc`, which
3525
- // `assetByKey` cannot resolve and which splits any per-asset
3526
- // aggregation away from the canonical `solana:usdc:<mint>` rows.
3527
- assetKey: assetKey(delegationAsset),
3528
- status: "completed",
3529
- submittedAt,
3530
- completedAt: Date.now(),
3531
- resultPreview: result.slice(0, RESULT_PREVIEW_MAX_LEN),
3532
- paymentSig: pullTx,
3533
- attachmentJson: resultAttachment ? JSON.stringify(resultAttachment) : void 0
3534
- });
3535
- const pullLine = pullTx !== void 0 ? `pull_tx=${pullTx}
3536
- ` : "";
3537
- const tip = buildJobCompletionTip(jobId, input.provider_npub);
3538
- return textResult(`event_id=${jobId}
3539
- ${pullLine}${result}${tip}`);
3540
- } catch (e) {
3541
- const msg = e instanceof Error ? e.message : String(e);
3542
- const isTimeout = e instanceof JobWaitTimeoutError;
3543
- await recordJobOutcome(agent, {
3544
- jobEventId: jobId,
3545
- capability: dTag,
3546
- providerPubkey,
3547
- providerName: clipProviderName(provider.name),
3548
- status: isTimeout ? "timeout" : "failed",
3549
- submittedAt,
3550
- completedAt: Date.now()
3551
- });
3552
- if (isTimeout) {
3553
- return textResult(
3554
- `event_id=${jobId}
3555
- Still processing (delegated: the provider pulls payment only after delivering the result). This is NOT an error - retry get_job_result with event_id="${jobId}" later. If the provider never starts, the proof expires within ${MAX_PROOF_TTL_SECS / 60} minutes with no charge.`
3556
- );
3557
- }
3558
- const safeMsg = sanitizeUntrusted(msg, "text").text;
3559
- return errorResult(
3560
- `Job ${jobId} failed: ${safeMsg}. Delegated jobs charge only on delivery - no charge occurred unless a pull was reported.`
3561
- );
3562
- }
3563
3670
  }
3564
3671
  }),
3565
3672
  defineTool({
@@ -3918,7 +4025,7 @@ ${text}`);
3918
4025
  }),
3919
4026
  defineTool({
3920
4027
  name: "submit_and_pay_job",
3921
- 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.',
4028
+ 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. This pays the listed price up front; when the capability advertises delegation, submit_delegated_job bills a metered card for actual usage instead.',
3922
4029
  schema: SubmitAndPayJobSchema,
3923
4030
  async handler(ctx, input) {
3924
4031
  ctx.toolRateLimiter.check();
@@ -3957,53 +4064,26 @@ ${text}`);
3957
4064
  }),
3958
4065
  defineTool({
3959
4066
  name: "submit_and_pay_job_from_file",
3960
- 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. Pass an optional `prompt` to send a text instruction alongside the file (e.g. how to edit an image); it rides inline (encrypted) while the file rides P2P.",
4067
+ 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. Pass an optional `prompt` to send a text instruction alongside the file (e.g. how to edit an image); it rides inline (encrypted) while the file rides P2P. This pays the listed price up front; when the capability advertises delegation, use submit_delegated_job_from_file so a metered card bills only actual usage.",
3961
4068
  schema: SubmitAndPayJobFromFileSchema,
3962
4069
  async handler(ctx, input) {
3963
4070
  ctx.toolRateLimiter.check();
3964
4071
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
3965
- const PROMPT_INLINE_CAP = 55e3;
3966
- const trimmedPrompt = input.prompt.trim();
3967
- if (utf8ByteLength(trimmedPrompt) > PROMPT_INLINE_CAP) {
3968
- return errorResult(
3969
- `Prompt is too long to ride inline (max ${PROMPT_INLINE_CAP} bytes); the file occupies the only attachment slot, so shorten the prompt.`
3970
- );
3971
- }
3972
- let prepared;
3973
- try {
3974
- prepared = await prepareFileInput(input.input_path, {
3975
- allowOutsideCwd: input.allow_outside_cwd
3976
- });
3977
- } catch (e) {
3978
- return errorResult(e instanceof Error ? e.message : String(e));
3979
- }
3980
- const agent = ctx.active();
3981
- if (agent.agentDir === void 0) {
3982
- return errorResult(
3983
- `Sending a file requires a persistent agent (this is an ephemeral session). Files are always transferred P2P via iroh, never inline.`
3984
- );
4072
+ const validated = await validateFileInput(ctx, {
4073
+ inputPath: input.input_path,
4074
+ allowOutsideCwd: input.allow_outside_cwd,
4075
+ prompt: input.prompt
4076
+ });
4077
+ if ("error" in validated) {
4078
+ return errorResult(validated.error);
3985
4079
  }
3986
- let attachment;
3987
- try {
3988
- const seeded = await ensureIrohTransport(agent).seedPath(prepared.absPath);
3989
- attachment = {
3990
- name: prepared.name,
3991
- size: seeded.size,
3992
- mime: prepared.mime,
3993
- transports: [{ kind: "iroh", ticket: seeded.ticket }]
3994
- };
3995
- } catch (e) {
3996
- const msg = e instanceof Error ? e.message : String(e);
3997
- if (/@number0\/iroh|iroh file transfer is unavailable/i.test(msg)) {
3998
- return errorResult(
3999
- `File transfer is unavailable: the optional @number0/iroh addon is not installed. Install it (e.g. \`bun add @number0/iroh\`) to send files.`
4000
- );
4001
- }
4002
- return errorResult(`Failed to seed file for transfer: ${msg}`);
4080
+ const seeded = await seedFileAttachment(validated.agent, validated.file);
4081
+ if ("error" in seeded) {
4082
+ return errorResult(seeded.error);
4003
4083
  }
4004
- return executeSubmitAndPay(ctx, agent, {
4005
- input: trimmedPrompt,
4006
- attachment,
4084
+ return executeSubmitAndPay(ctx, validated.agent, {
4085
+ input: validated.prompt,
4086
+ attachment: seeded.attachment,
4007
4087
  providerNpub: input.provider_npub,
4008
4088
  providerPubkey: decodeNpub(input.provider_npub),
4009
4089
  capability: input.capability,
@@ -4730,11 +4810,11 @@ var discoveryTools = [
4730
4810
  // Metered pricing: `job_price` above is then the CEILING, and the
4731
4811
  // real charge lands between the floor and it. Surfaced so a buying
4732
4812
  // model does not read the ceiling as a flat rate and skip a card
4733
- // that is usually far cheaper. Only ever reachable through
4734
- // submit_delegated_job - the ordinary paid path settles up front
4735
- // and always collects the ceiling.
4813
+ // that is usually far cheaper. Only ever reachable through the
4814
+ // delegated tools - the ordinary paid path settles up front and
4815
+ // always collects the ceiling.
4736
4816
  // Gated on `delegation` too: metering is only reachable through
4737
- // `submit_delegated_job`, which refuses a card with no delegation
4817
+ // the delegated tools, which refuse a card with no delegation
4738
4818
  // descriptor outright. Advertising "billed for actual usage
4739
4819
  // (requires delegated payment)" on such a card would point a
4740
4820
  // buying model at a door that is bolted shut.
@@ -4750,7 +4830,8 @@ var discoveryTools = [
4750
4830
  network: card.payment?.network,
4751
4831
  network_fee_estimate_sol: gasEstimate,
4752
4832
  // File-exchange hints (dynamic-script). Informational: the MCP/CLI
4753
- // CAN send files via submit_and_pay_job_from_file, so this does not
4833
+ // CAN send files via submit_and_pay_job_from_file and
4834
+ // submit_delegated_job_from_file, so this does not
4754
4835
  // gate anything - it just tells the caller a file input is expected.
4755
4836
  // Already length-bounded by parseCapabilityEvent.
4756
4837
  ...card.inputMime ? { input_mime: card.inputMime } : {},