@elisym/mcp 0.24.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';
@@ -22,81 +22,7 @@ import { fileURLToPath } from 'node:url';
22
22
  import { getTransferSolInstruction } from '@solana-program/system';
23
23
  import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, getCreateAssociatedTokenIdempotentInstruction, ASSOCIATED_TOKEN_PROGRAM_ADDRESS, getTransferCheckedInstruction } from '@solana-program/token';
24
24
 
25
- function coerceNetwork(raw, name) {
26
- if (raw === void 0 || raw === "devnet") {
27
- return "devnet";
28
- }
29
- if (raw === "mainnet") {
30
- return "mainnet";
31
- }
32
- throw new Error(
33
- `Agent "${name}" has unsupported network "${raw}". Expected "devnet" or "mainnet".`
34
- );
35
- }
36
- async function loadAgentConfig(name, passphrase) {
37
- validateAgentName(name);
38
- const loaded = await loadAgent(name, process.cwd(), passphrase);
39
- const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
40
- const network = coerceNetwork(solPayment?.network, name);
41
- return {
42
- nostrSecretKey: loaded.secrets.nostr_secret_key,
43
- solanaSecretKey: loaded.secrets.solana_secret_key,
44
- relays: loaded.yaml.relays.length > 0 ? loaded.yaml.relays : void 0,
45
- network,
46
- payments: loaded.yaml.payments.length > 0 ? loaded.yaml.payments.map((payment2) => ({
47
- chain: payment2.chain,
48
- network: payment2.network,
49
- address: payment2.address
50
- })) : void 0,
51
- security: {
52
- withdrawals_enabled: loaded.yaml.security.withdrawals_enabled,
53
- agent_switch_enabled: loaded.yaml.security.agent_switch_enabled
54
- },
55
- encrypted: loaded.encrypted
56
- };
57
- }
58
- async function saveAgentConfig(name, input) {
59
- validateAgentName(name);
60
- const created = await createAgentDir({ target: "home", name, cwd: process.cwd() });
61
- const network = input.network ?? "devnet";
62
- try {
63
- await writeFile(created.paths.yaml, "", { flag: "wx", mode: 420 });
64
- } catch (error) {
65
- if (error instanceof Error && "code" in error && error.code === "EEXIST") {
66
- throw new Error(`Agent "${name}" already exists.`);
67
- }
68
- throw error;
69
- }
70
- await writeYamlInitial(created.dir, {
71
- display_name: void 0,
72
- description: input.description,
73
- picture: void 0,
74
- banner: void 0,
75
- relays: input.relays,
76
- payments: input.solanaAddress ? [{ chain: "solana", network, address: input.solanaAddress }] : [],
77
- llm: void 0,
78
- security: input.security ?? {}
79
- });
80
- await writeExampleSkillTemplate(created.dir);
81
- await writeSecrets(
82
- created.dir,
83
- {
84
- nostr_secret_key: input.nostrSecretKey,
85
- solana_secret_key: input.solanaSecretKey
86
- },
87
- input.passphrase
88
- );
89
- }
90
- async function updateAgentSecurity(name, patch, passphrase) {
91
- const loaded = await loadAgent(name, process.cwd(), passphrase);
92
- const merged = { ...loaded.yaml.security, ...patch };
93
- await writeYaml(loaded.dir, { ...loaded.yaml, security: merged });
94
- return merged;
95
- }
96
- async function listAgentNames() {
97
- const agents = await listAgents(process.cwd());
98
- return agents.map((agent) => agent.name);
99
- }
25
+ var DEFAULT_NETWORK = "mainnet";
100
26
  function rpcUrlFor(network) {
101
27
  return network === "mainnet" ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com";
102
28
  }
@@ -309,6 +235,83 @@ function takeSpendWarnings(ctx, asset) {
309
235
  ctx.sessionSpendWarnings.set(key, fired);
310
236
  return lines;
311
237
  }
238
+
239
+ // src/config.ts
240
+ function coerceNetwork(raw, name) {
241
+ if (raw === void 0 || raw === "devnet") {
242
+ return "devnet";
243
+ }
244
+ if (raw === "mainnet") {
245
+ return "mainnet";
246
+ }
247
+ throw new Error(
248
+ `Agent "${name}" has unsupported network "${raw}". Expected "devnet" or "mainnet".`
249
+ );
250
+ }
251
+ async function loadAgentConfig(name, passphrase) {
252
+ validateAgentName(name);
253
+ const loaded = await loadAgent(name, process.cwd(), passphrase);
254
+ const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
255
+ const network = coerceNetwork(solPayment?.network, name);
256
+ return {
257
+ nostrSecretKey: loaded.secrets.nostr_secret_key,
258
+ solanaSecretKey: loaded.secrets.solana_secret_key,
259
+ relays: loaded.yaml.relays.length > 0 ? loaded.yaml.relays : void 0,
260
+ network,
261
+ payments: loaded.yaml.payments.length > 0 ? loaded.yaml.payments.map((payment2) => ({
262
+ chain: payment2.chain,
263
+ network: payment2.network,
264
+ address: payment2.address
265
+ })) : void 0,
266
+ security: {
267
+ withdrawals_enabled: loaded.yaml.security.withdrawals_enabled,
268
+ agent_switch_enabled: loaded.yaml.security.agent_switch_enabled
269
+ },
270
+ encrypted: loaded.encrypted
271
+ };
272
+ }
273
+ async function saveAgentConfig(name, input) {
274
+ validateAgentName(name);
275
+ const created = await createAgentDir({ target: "home", name, cwd: process.cwd() });
276
+ const network = input.network ?? DEFAULT_NETWORK;
277
+ try {
278
+ await writeFile(created.paths.yaml, "", { flag: "wx", mode: 420 });
279
+ } catch (error) {
280
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
281
+ throw new Error(`Agent "${name}" already exists.`);
282
+ }
283
+ throw error;
284
+ }
285
+ await writeYamlInitial(created.dir, {
286
+ display_name: void 0,
287
+ description: input.description,
288
+ picture: void 0,
289
+ banner: void 0,
290
+ relays: input.relays,
291
+ payments: input.solanaAddress ? [{ chain: "solana", network, address: input.solanaAddress }] : [],
292
+ llm: void 0,
293
+ security: input.security ?? {}
294
+ });
295
+ await writeExampleSkillTemplate(created.dir);
296
+ await writeSecrets(
297
+ created.dir,
298
+ {
299
+ nostr_secret_key: input.nostrSecretKey,
300
+ solana_secret_key: input.solanaSecretKey
301
+ },
302
+ input.passphrase
303
+ );
304
+ }
305
+ async function updateAgentSecurity(name, patch, passphrase) {
306
+ const loaded = await loadAgent(name, process.cwd(), passphrase);
307
+ const merged = { ...loaded.yaml.security, ...patch };
308
+ await writeYaml(loaded.dir, { ...loaded.yaml, security: merged });
309
+ return merged;
310
+ }
311
+ async function listAgentNames() {
312
+ const agents = await listAgents(process.cwd());
313
+ return agents.map((agent) => agent.name);
314
+ }
312
315
  async function writeFileAtomic(path, content, mode = 384) {
313
316
  const tmpPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
314
317
  try {
@@ -1261,8 +1264,8 @@ var CreateAgentSchema = z.object({
1261
1264
  // customer-mode in 0.1.x and never publishes a NIP-89 capability card,
1262
1265
  // so an advertised capability list would be misleading. Provider-mode
1263
1266
  // (0.2.0) will reintroduce this field.
1264
- network: z.enum(["devnet", "mainnet"]).default("devnet").describe(
1265
- "Solana network this agent is bound to. FIXED AT CREATION: an agent can never change networks - switching networks means creating (or switch_agent-ing to) another agent bound to the other network. Default devnet; mainnet payments move real funds."
1267
+ network: z.enum(["devnet", "mainnet"]).default(DEFAULT_NETWORK).describe(
1268
+ `Solana network this agent is bound to. FIXED AT CREATION: an agent can never change networks - switching networks means creating (or switch_agent-ing to) another agent bound to the other network. Defaults to ${DEFAULT_NETWORK}, where payments move REAL funds; pass "devnet" for an agent to experiment with.`
1266
1269
  ),
1267
1270
  passphrase: z.string().optional().describe("Optional passphrase; if set, secret keys are encrypted at rest."),
1268
1271
  activate: z.boolean().default(true)
@@ -2516,6 +2519,11 @@ var SubmitAndPayJobFromFileSchema = z.object({
2516
2519
  ),
2517
2520
  session_id: SessionIdSchema
2518
2521
  });
2522
+ var SubmitDelegatedJobFromFileSchema = SubmitAndPayJobFromFileSchema.omit({
2523
+ session_id: true
2524
+ }).extend({
2525
+ max_price_lamports: SubmitDelegatedJobSchema.shape.max_price_lamports
2526
+ });
2519
2527
  var SubmitDiffReviewSchema = z.object({
2520
2528
  provider_npub: z.string(),
2521
2529
  capability: z.string().min(1).max(64).default("review").describe('Capability tag advertised by the reviewer. Override if not "review".'),
@@ -2696,6 +2704,9 @@ ${formatNetworkBaseline(baseline)}${statsHint}`;
2696
2704
  return "";
2697
2705
  }
2698
2706
  }
2707
+ function isDelegatedTool(toolName) {
2708
+ return toolName === "submit_delegated_job" || toolName === "submit_delegated_job_from_file";
2709
+ }
2699
2710
  async function confirmPriceGate(opts) {
2700
2711
  const {
2701
2712
  agent,
@@ -2717,7 +2728,7 @@ async function confirmPriceGate(opts) {
2717
2728
  if (price > 0 && maxPriceLamports === void 0) {
2718
2729
  const gasLine = await gasHintForCardAsset(agent, asset);
2719
2730
  const subject = toolName === "buy_capability" ? `Capability "${capability}" from "${providerLabel}"` : `Job for capability "${capability}" from "${providerLabel}"`;
2720
- 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))}.`;
2721
2732
  const { text } = sanitizeUntrusted(
2722
2733
  `${costLine}${gasLine}
2723
2734
 
@@ -2994,6 +3005,51 @@ async function prepareTextInput(agent, text, options) {
2994
3005
  };
2995
3006
  }
2996
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
+ }
2997
3053
  async function executeSubmitAndPay(ctx, agent, params) {
2998
3054
  const ping = await agent.client.ping.pingAgent(params.providerPubkey, PRE_PING_TIMEOUT_MS);
2999
3055
  if (!ping.online) {
@@ -3222,6 +3278,234 @@ ${sessionIdLine(sessionId).trimEnd()}` : "";
3222
3278
  );
3223
3279
  }
3224
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
+ }
3225
3509
  function awaitJobResult(agent, options, fn, safetyTimeoutMs) {
3226
3510
  return new Promise((resolve, reject) => {
3227
3511
  let closeFn = null;
@@ -3330,233 +3614,59 @@ var customerTools = [
3330
3614
  }),
3331
3615
  defineTool({
3332
3616
  name: "submit_delegated_job",
3333
- 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.",
3334
3618
  schema: SubmitDelegatedJobSchema,
3335
3619
  async handler(ctx, input) {
3336
3620
  ctx.toolRateLimiter.check();
3337
- checkLen("input", input.input, MAX_INPUT_LEN);
3338
3621
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
3339
- const agent = ctx.active();
3340
- if (!agent.solanaKeypair) {
3341
- return errorResult(
3342
- "Solana wallet not configured for this agent - delegated payment signs the proof with the owner (wallet) key."
3343
- );
3344
- }
3345
- const providerPubkey = decodeNpub(input.provider_npub);
3346
- const dTag = toDTag(input.capability);
3347
- const timeoutMs = Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3;
3348
- const ping = await agent.client.ping.pingAgent(providerPubkey, PRE_PING_TIMEOUT_MS);
3349
- if (!ping.online) {
3350
- return errorResult(
3351
- `Provider ${input.provider_npub} is offline. Run search_agents to find currently-online providers.`
3352
- );
3353
- }
3354
- const providers = await agent.client.discovery.fetchAgents(agent.network);
3355
- const provider = providers.find((candidate) => candidate.npub === input.provider_npub);
3356
- if (!provider) {
3357
- return errorResult(
3358
- `Provider ${input.provider_npub} not found on ${agent.network}. Refresh discovery (e.g. search_agents) or verify the npub is correct.`
3359
- );
3360
- }
3361
- const card = paymentCardForCapability(provider, dTag);
3362
- const descriptor = card?.delegation;
3363
- if (!descriptor) {
3364
- return errorResult(
3365
- `Capability "${input.capability}" of ${input.provider_npub} does not advertise delegated payment. Use submit_and_pay_job instead.`
3366
- );
3367
- }
3368
- const { price, asset } = advertisedPriceForCapability(provider, dTag);
3369
- if (price <= 0) {
3370
- return errorResult(
3371
- `Capability "${input.capability}" advertises no price - delegated payment needs a priced skill. Use create_job for free capabilities.`
3372
- );
3373
- }
3374
- const delegationAsset = resolveUsdcAsset(agent.network);
3375
- const cardPayment = paymentCardForCapability(provider, dTag)?.payment;
3376
- const cardMint = cardPayment?.mint;
3377
- const isCanonicalUsdc = (cardPayment?.chain ?? delegationAsset.chain) === delegationAsset.chain && cardPayment?.token?.toLowerCase() === delegationAsset.token && (cardMint === void 0 || cardMint === delegationAsset.mint);
3378
- if (!isCanonicalUsdc) {
3379
- const declared = cardPayment?.symbol ?? cardPayment?.token ?? asset.symbol;
3380
- const { text } = sanitizeUntrusted(
3381
- `Delegated payment is USDC-only, but this capability is priced in ${declared}.`,
3382
- "text"
3383
- );
3384
- return errorResult(text);
3385
- }
3386
- const priceSubunits = BigInt(price);
3387
- const rpc = createSolanaRpc(rpcUrlFor(agent.network));
3388
- const ownerAta = await deriveOwnerDelegationAta(agent.solanaKeypair.publicKey, agent.network);
3389
- let delegation;
3390
- try {
3391
- delegation = await getDelegation(rpc, ownerAta);
3392
- } catch (e) {
3393
- const message = e instanceof Error ? e.message : String(e);
3394
- return errorResult(`Could not read your delegation on-chain: ${message}`);
3395
- }
3396
- if (delegation === null || delegation.delegate === null) {
3397
- return errorResult(
3398
- "No active delegation from this wallet. Approve one first (web app Delegation tab, or the approve flow), then retry."
3399
- );
3400
- }
3401
- if (delegation.delegate !== descriptor.delegate_pubkey) {
3402
- const { text } = sanitizeUntrusted(
3403
- `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.`,
3404
- "text"
3405
- );
3406
- return errorResult(text);
3407
- }
3408
- if (delegation.remainingCap < priceSubunits) {
3409
- return errorResult(
3410
- `Remaining delegated cap ${formatAssetAmount(delegationAsset, delegation.remainingCap)} is below the price ${formatAssetAmount(delegationAsset, priceSubunits)}. Top up the delegation (re-approve) first.`
3411
- );
3412
- }
3413
- if (delegation.balance < priceSubunits) {
3622
+ const inputBytes = utf8ByteLength(input.input);
3623
+ if (inputBytes > LIMITS.MAX_REINLINE_TEXT_BYTES) {
3414
3624
  return errorResult(
3415
- `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.`
3416
3626
  );
3417
3627
  }
3418
- try {
3419
- assertCanSpend(ctx, delegationAsset, priceSubunits);
3420
- } catch (e) {
3421
- return errorResult(e instanceof Error ? e.message : String(e));
3422
- }
3423
- const priceGate = await confirmPriceGate({
3424
- agent,
3425
- 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,
3426
3633
  capability: input.capability,
3427
- price,
3428
- asset: delegationAsset,
3634
+ kindOffset: input.kind_offset,
3635
+ timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
3429
3636
  maxPriceLamports: input.max_price_lamports,
3430
- toolName: "submit_delegated_job",
3431
- // Card-published floor, so the quote reads "from X, never more than Y"
3432
- // instead of a flat number the buyer will usually not be charged.
3433
- ...card?.metered !== void 0 ? { meteredMinSubunits: BigInt(card.metered.min_subunits) } : {}
3637
+ toolName: "submit_delegated_job"
3434
3638
  });
3435
- if (priceGate) {
3436
- return priceGate;
3437
- }
3438
- const ownerSigner = await createKeyPairSignerFromBytes(agent.solanaKeypair.secretKey);
3439
- const expiryUnix = Math.floor(Date.now() / 1e3) + MAX_PROOF_TTL_SECS;
3440
- const nonce = mintDelegationNonce();
3441
- const proof = await buildDelegationAuthProof({
3442
- ownerSigner,
3443
- agentDelegate: descriptor.delegate_pubkey,
3444
- nostrAuthor: agent.identity.publicKey,
3445
- owner: agent.solanaKeypair.publicKey,
3446
- expiryUnix,
3447
- 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
3448
3652
  });
3449
- const submittedAt = Date.now();
3450
- const jobId = await agent.client.marketplace.submitJobRequest(agent.identity, {
3451
- input: input.input,
3452
- capability: dTag,
3453
- 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,
3454
3665
  kindOffset: input.kind_offset,
3455
- acceptTransports: MCP_ACCEPT_TRANSPORTS,
3456
- delegatedPayment: {
3457
- owner: agent.solanaKeypair.publicKey,
3458
- expiryUnix,
3459
- nonce,
3460
- proof
3461
- }
3666
+ timeoutMs: Math.min(input.timeout_secs, MAX_TIMEOUT_SECS) * 1e3,
3667
+ maxPriceLamports: input.max_price_lamports,
3668
+ toolName: "submit_delegated_job_from_file"
3462
3669
  });
3463
- let resultAttachment;
3464
- let pullTx;
3465
- let settledAmountSubunits;
3466
- try {
3467
- const result = await awaitJobResult(
3468
- agent,
3469
- {},
3470
- ({ resolve, reject }) => ({
3471
- jobEventId: jobId,
3472
- providerPubkey,
3473
- customerPublicKey: agent.identity.publicKey,
3474
- callbacks: {
3475
- onResult(content, _eventId, attachment, _attachments, paymentTx, paidAmountSubunits) {
3476
- pullTx = paymentTx;
3477
- settledAmountSubunits = paidAmountSubunits;
3478
- if (attachment) {
3479
- resultAttachment = attachment;
3480
- resolve(formatFileResultMetadata(jobId, attachment));
3481
- return;
3482
- }
3483
- const sanitized = sanitizeResultContent(content);
3484
- resolve(`Job completed.
3485
-
3486
- ${sanitized.text}`);
3487
- },
3488
- onError(error) {
3489
- rejectWithProviderError(reject, error);
3490
- },
3491
- onTimeout(waitedMs) {
3492
- reject(new JobWaitTimeoutError(waitedMs));
3493
- }
3494
- },
3495
- timeoutMs,
3496
- customerSecretKey: agent.identity.secretKey
3497
- }),
3498
- timeoutMs + 5e3
3499
- );
3500
- const settledForSession = settledSubunitsForHistory(card, price, settledAmountSubunits);
3501
- recordSpend(ctx, delegationAsset, BigInt(settledForSession));
3502
- await recordJobOutcome(agent, {
3503
- jobEventId: jobId,
3504
- capability: dTag,
3505
- providerPubkey,
3506
- providerName: clipProviderName(provider.name),
3507
- // What actually moved, as recorded in the buyer's OWN history.
3508
- //
3509
- // On a FLAT card the runtime always pulls exactly `price`, so there is
3510
- // nothing to learn from the provider and the reported figure is only a
3511
- // way to lie - the card price is the truth by construction.
3512
- //
3513
- // On a METERED card the pull is variable, so the provider's report is
3514
- // the only source of the real number. It is still attacker-controlled
3515
- // and arrives through a tag parser that accepts negatives and
3516
- // prefix-parses garbage, so it is trusted only inside the range the
3517
- // card published and the buyer approved: `[min, price]`. Anything
3518
- // outside degrades to the ceiling rather than to the attacker's value.
3519
- paidAmountSubunits: String(settledForSession),
3520
- // The registry key, not the card's. A mint-less `usdc` card is
3521
- // accepted by the gate above but keys as `solana:usdc`, which
3522
- // `assetByKey` cannot resolve and which splits any per-asset
3523
- // aggregation away from the canonical `solana:usdc:<mint>` rows.
3524
- assetKey: assetKey(delegationAsset),
3525
- status: "completed",
3526
- submittedAt,
3527
- completedAt: Date.now(),
3528
- resultPreview: result.slice(0, RESULT_PREVIEW_MAX_LEN),
3529
- paymentSig: pullTx,
3530
- attachmentJson: resultAttachment ? JSON.stringify(resultAttachment) : void 0
3531
- });
3532
- const pullLine = pullTx !== void 0 ? `pull_tx=${pullTx}
3533
- ` : "";
3534
- const tip = buildJobCompletionTip(jobId, input.provider_npub);
3535
- return textResult(`event_id=${jobId}
3536
- ${pullLine}${result}${tip}`);
3537
- } catch (e) {
3538
- const msg = e instanceof Error ? e.message : String(e);
3539
- const isTimeout = e instanceof JobWaitTimeoutError;
3540
- await recordJobOutcome(agent, {
3541
- jobEventId: jobId,
3542
- capability: dTag,
3543
- providerPubkey,
3544
- providerName: clipProviderName(provider.name),
3545
- status: isTimeout ? "timeout" : "failed",
3546
- submittedAt,
3547
- completedAt: Date.now()
3548
- });
3549
- if (isTimeout) {
3550
- return textResult(
3551
- `event_id=${jobId}
3552
- 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.`
3553
- );
3554
- }
3555
- const safeMsg = sanitizeUntrusted(msg, "text").text;
3556
- return errorResult(
3557
- `Job ${jobId} failed: ${safeMsg}. Delegated jobs charge only on delivery - no charge occurred unless a pull was reported.`
3558
- );
3559
- }
3560
3670
  }
3561
3671
  }),
3562
3672
  defineTool({
@@ -3915,7 +4025,7 @@ ${text}`);
3915
4025
  }),
3916
4026
  defineTool({
3917
4027
  name: "submit_and_pay_job",
3918
- 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.',
3919
4029
  schema: SubmitAndPayJobSchema,
3920
4030
  async handler(ctx, input) {
3921
4031
  ctx.toolRateLimiter.check();
@@ -3954,53 +4064,26 @@ ${text}`);
3954
4064
  }),
3955
4065
  defineTool({
3956
4066
  name: "submit_and_pay_job_from_file",
3957
- 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.",
3958
4068
  schema: SubmitAndPayJobFromFileSchema,
3959
4069
  async handler(ctx, input) {
3960
4070
  ctx.toolRateLimiter.check();
3961
4071
  checkLen("provider_npub", input.provider_npub, MAX_NPUB_LEN);
3962
- const PROMPT_INLINE_CAP = 55e3;
3963
- const trimmedPrompt = input.prompt.trim();
3964
- if (utf8ByteLength(trimmedPrompt) > PROMPT_INLINE_CAP) {
3965
- return errorResult(
3966
- `Prompt is too long to ride inline (max ${PROMPT_INLINE_CAP} bytes); the file occupies the only attachment slot, so shorten the prompt.`
3967
- );
3968
- }
3969
- let prepared;
3970
- try {
3971
- prepared = await prepareFileInput(input.input_path, {
3972
- allowOutsideCwd: input.allow_outside_cwd
3973
- });
3974
- } catch (e) {
3975
- return errorResult(e instanceof Error ? e.message : String(e));
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);
3976
4079
  }
3977
- const agent = ctx.active();
3978
- if (agent.agentDir === void 0) {
3979
- return errorResult(
3980
- `Sending a file requires a persistent agent (this is an ephemeral session). Files are always transferred P2P via iroh, never inline.`
3981
- );
4080
+ const seeded = await seedFileAttachment(validated.agent, validated.file);
4081
+ if ("error" in seeded) {
4082
+ return errorResult(seeded.error);
3982
4083
  }
3983
- let attachment;
3984
- try {
3985
- const seeded = await ensureIrohTransport(agent).seedPath(prepared.absPath);
3986
- attachment = {
3987
- name: prepared.name,
3988
- size: seeded.size,
3989
- mime: prepared.mime,
3990
- transports: [{ kind: "iroh", ticket: seeded.ticket }]
3991
- };
3992
- } catch (e) {
3993
- const msg = e instanceof Error ? e.message : String(e);
3994
- if (/@number0\/iroh|iroh file transfer is unavailable/i.test(msg)) {
3995
- return errorResult(
3996
- `File transfer is unavailable: the optional @number0/iroh addon is not installed. Install it (e.g. \`bun add @number0/iroh\`) to send files.`
3997
- );
3998
- }
3999
- return errorResult(`Failed to seed file for transfer: ${msg}`);
4000
- }
4001
- return executeSubmitAndPay(ctx, agent, {
4002
- input: trimmedPrompt,
4003
- attachment,
4084
+ return executeSubmitAndPay(ctx, validated.agent, {
4085
+ input: validated.prompt,
4086
+ attachment: seeded.attachment,
4004
4087
  providerNpub: input.provider_npub,
4005
4088
  providerPubkey: decodeNpub(input.provider_npub),
4006
4089
  capability: input.capability,
@@ -4727,11 +4810,11 @@ var discoveryTools = [
4727
4810
  // Metered pricing: `job_price` above is then the CEILING, and the
4728
4811
  // real charge lands between the floor and it. Surfaced so a buying
4729
4812
  // model does not read the ceiling as a flat rate and skip a card
4730
- // that is usually far cheaper. Only ever reachable through
4731
- // submit_delegated_job - the ordinary paid path settles up front
4732
- // 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.
4733
4816
  // Gated on `delegation` too: metering is only reachable through
4734
- // `submit_delegated_job`, which refuses a card with no delegation
4817
+ // the delegated tools, which refuse a card with no delegation
4735
4818
  // descriptor outright. Advertising "billed for actual usage
4736
4819
  // (requires delegated payment)" on such a card would point a
4737
4820
  // buying model at a door that is bolted shut.
@@ -4747,7 +4830,8 @@ var discoveryTools = [
4747
4830
  network: card.payment?.network,
4748
4831
  network_fee_estimate_sol: gasEstimate,
4749
4832
  // File-exchange hints (dynamic-script). Informational: the MCP/CLI
4750
- // 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
4751
4835
  // gate anything - it just tells the caller a file input is expected.
4752
4836
  // Already length-bounded by parseCapabilityEvent.
4753
4837
  ...card.inputMime ? { input_mime: card.inputMime } : {},
@@ -7205,7 +7289,7 @@ program.action(
7205
7289
  }
7206
7290
  const client = new ElisymClient({ relays: RELAYS });
7207
7291
  const name = process.env.ELISYM_AGENT_NAME ?? "mcp-agent";
7208
- const network = envNetwork ?? "devnet";
7292
+ const network = envNetwork ?? DEFAULT_NETWORK;
7209
7293
  ctx.register({ client, identity, name, network, security: {} });
7210
7294
  console.error(`Ephemeral agent: ${name} (${network})`);
7211
7295
  } else {
@@ -7225,7 +7309,7 @@ program.action(
7225
7309
  } else {
7226
7310
  const identity = ElisymIdentity.generate();
7227
7311
  const client = new ElisymClient({ relays: RELAYS });
7228
- const network = envNetwork ?? "devnet";
7312
+ const network = envNetwork ?? DEFAULT_NETWORK;
7229
7313
  ctx.register({ client, identity, name: "mcp-agent", network, security: {} });
7230
7314
  console.error(`Created ephemeral agent (no persistent identity, ${network}).`);
7231
7315
  }
@@ -7235,8 +7319,8 @@ program.action(
7235
7319
  );
7236
7320
  program.command("init [name]").description("Create a new agent identity").option("-d, --description <desc>", "Agent description", "Elisym MCP agent").option(
7237
7321
  "-n, --network <network>",
7238
- "Solana network (devnet or mainnet). Fixed at creation: to change networks later, create a new agent. Mainnet payments move real funds.",
7239
- "devnet"
7322
+ `Solana network (devnet or mainnet). Fixed at creation: to change networks later, create a new agent. Defaults to ${DEFAULT_NETWORK}; mainnet payments move real funds.`,
7323
+ DEFAULT_NETWORK
7240
7324
  ).option("--install", "Also install into MCP clients").option(
7241
7325
  "--passphrase <value>",
7242
7326
  'Passphrase to encrypt secret keys at rest. Empty string ("") skips encryption. Also reads ELISYM_PASSPHRASE env var. When neither is provided, prompts interactively.'
@@ -7262,7 +7346,7 @@ program.command("init [name]").description("Create a new agent identity").option
7262
7346
  name: "network",
7263
7347
  message: "Solana network (fixed at creation - create a new agent to change it):",
7264
7348
  choices: ["devnet", "mainnet"],
7265
- default: "devnet"
7349
+ default: DEFAULT_NETWORK
7266
7350
  }
7267
7351
  ]);
7268
7352
  name = answers.name;