@elisym/cli 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools
9
9
  import YAML2 from 'yaml';
10
10
  import { Command } from 'commander';
11
11
  import { mkdir, writeFile, readFile, mkdtemp, rm, rename, stat } from 'node:fs/promises';
12
- import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
12
+ import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, OnchainCallSkill, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
13
13
  import chalk from 'chalk';
14
14
  import Decimal from 'decimal.js-light';
15
15
  import { decodePaymentRequiredHeader } from '@x402/core/http';
@@ -5318,7 +5318,15 @@ var AgentRuntime = class {
5318
5318
  return;
5319
5319
  }
5320
5320
  this.ledger.recordDeliveredContent(job.jobId, deliveredContent);
5321
+ delegatedPull.chargedSubunits = this.resolveMeteredCharge(
5322
+ job,
5323
+ matched,
5324
+ delegatedPull.priceSubunits,
5325
+ output.chargeSubunits,
5326
+ log
5327
+ );
5321
5328
  const outcome = await this.executeDelegatedPull(job, delegatedPull, log);
5329
+ netAmount = Number(delegatedPull.chargedSubunits ?? delegatedPull.priceSubunits);
5322
5330
  if (outcome === "dead") {
5323
5331
  this.ledger.markFailed(job.jobId);
5324
5332
  await this.transport.sendFeedback(job, {
@@ -5353,6 +5361,59 @@ var AgentRuntime = class {
5353
5361
  }
5354
5362
  }
5355
5363
  }
5364
+ /**
5365
+ * Decide what a delegated pull should move, for a skill that opted into
5366
+ * metered pricing.
5367
+ *
5368
+ * The skill reports a figure through `ELISYM_CHARGE_FILE`; we clamp it into
5369
+ * the range the CARD published, so the buyer can never be billed above the
5370
+ * `price` they approved nor below the `min` they were quoted. A skill that is
5371
+ * not metered, or one that reported nothing usable, charges the ceiling -
5372
+ * that is exactly today's behaviour and exactly what the buyer consented to,
5373
+ * so a provider-side bug costs the provider nothing and surprises no one.
5374
+ * The "reported nothing" case is logged loudly because on a metered skill it
5375
+ * means the script is broken, not that the job was cheap.
5376
+ *
5377
+ * Returns `undefined` to mean "charge the ceiling", which is what every
5378
+ * caller already falls back to.
5379
+ */
5380
+ resolveMeteredCharge(job, skill, ceiling, reported, log) {
5381
+ const floor = skill?.meteredMinSubunits;
5382
+ if (floor === void 0) {
5383
+ return void 0;
5384
+ }
5385
+ if (typeof floor !== "bigint" || floor <= 0n) {
5386
+ log(
5387
+ `[${job.jobId.slice(0, 8)}] Metered skill has an unusable floor (${String(floor)}) - billing the ${ceiling} ceiling.`
5388
+ );
5389
+ return void 0;
5390
+ }
5391
+ if (reported !== void 0 && typeof reported !== "bigint") {
5392
+ log(
5393
+ `[${job.jobId.slice(0, 8)}] Metered skill reported a non-bigint charge - billing the ${ceiling} ceiling.`
5394
+ );
5395
+ return void 0;
5396
+ }
5397
+ if (reported === void 0) {
5398
+ log(
5399
+ `[${job.jobId.slice(0, 8)}] Metered skill reported no charge - billing the ${ceiling} ceiling. The script should write its cost in subunits to ELISYM_CHARGE_FILE.`
5400
+ );
5401
+ return void 0;
5402
+ }
5403
+ let charged = reported;
5404
+ if (charged < floor) {
5405
+ charged = floor;
5406
+ }
5407
+ if (charged > ceiling) {
5408
+ charged = ceiling;
5409
+ }
5410
+ if (charged !== reported) {
5411
+ log(
5412
+ `[${job.jobId.slice(0, 8)}] Metered charge ${reported} clamped to ${charged} (published range ${floor}..${ceiling}).`
5413
+ );
5414
+ }
5415
+ return charged;
5416
+ }
5356
5417
  reserveDelegation(owner, amount) {
5357
5418
  this.delegationInFlight.set(owner, (this.delegationInFlight.get(owner) ?? 0n) + amount);
5358
5419
  }
@@ -5484,13 +5545,13 @@ var AgentRuntime = class {
5484
5545
  "the delegation account balance is below this skill price."
5485
5546
  );
5486
5547
  }
5487
- this.reserveDelegation(request.owner, priceSubunits);
5488
5548
  log(
5489
5549
  `[${job.jobId.slice(0, 8)}] Delegated pre-check ok: ${formatAssetAmount(
5490
5550
  delegationAsset,
5491
5551
  priceSubunits
5492
5552
  )} from ${request.owner.slice(0, 8)}... (pull after work)`
5493
5553
  );
5554
+ this.reserveDelegation(request.owner, priceSubunits);
5494
5555
  return { request, delegateSigner, ownerAta, priceSubunits, rpc, network };
5495
5556
  }
5496
5557
  /**
@@ -5504,11 +5565,12 @@ var AgentRuntime = class {
5504
5565
  throw new Error("Solana address not configured");
5505
5566
  }
5506
5567
  const destinationAta = await deriveOwnerDelegationAta(this.config.solanaAddress, ctx.network);
5568
+ const amount = ctx.chargedSubunits ?? ctx.priceSubunits;
5507
5569
  const instructions = await buildDelegatedTransfer({
5508
5570
  delegate: ctx.delegateSigner,
5509
5571
  source: ctx.ownerAta,
5510
5572
  destination: destinationAta,
5511
- amount: ctx.priceSubunits,
5573
+ amount,
5512
5574
  network: ctx.network,
5513
5575
  // Idempotent create (delegate-paid rent) so a fresh provider wallet's
5514
5576
  // first delegated job does not dead-letter on a missing ATA.
@@ -5521,11 +5583,13 @@ var AgentRuntime = class {
5521
5583
  job.jobId,
5522
5584
  pull.signature,
5523
5585
  Number(pull.lastValidBlockHeight),
5524
- Number(ctx.priceSubunits)
5586
+ Number(amount)
5525
5587
  );
5526
5588
  ctx.pullSignature = pull.signature;
5527
5589
  const outcome = await sendConfirmToTerminal(ctx.rpc, pull, {});
5528
- log(`[${job.jobId.slice(0, 8)}] Delegated pull ${outcome}: ${pull.signature.slice(0, 16)}...`);
5590
+ log(
5591
+ `[${job.jobId.slice(0, 8)}] Delegated pull ${outcome}: ${amount} subunits, ${pull.signature.slice(0, 16)}...`
5592
+ );
5529
5593
  return outcome;
5530
5594
  }
5531
5595
  /**
@@ -6704,6 +6768,51 @@ var DynamicScriptSkill = class {
6704
6768
  return this.inner.execute(input, ctx);
6705
6769
  }
6706
6770
  };
6771
+ var OnchainSkill = class {
6772
+ name;
6773
+ description;
6774
+ capabilities;
6775
+ priceSubunits;
6776
+ asset;
6777
+ mode = "onchain";
6778
+ image;
6779
+ imageFile;
6780
+ dir;
6781
+ llmOverride;
6782
+ onchain;
6783
+ inner;
6784
+ constructor(params) {
6785
+ this.name = params.name;
6786
+ this.description = params.description;
6787
+ this.capabilities = params.capabilities;
6788
+ this.priceSubunits = params.priceSubunits;
6789
+ this.asset = params.asset;
6790
+ this.image = params.image;
6791
+ this.imageFile = params.imageFile;
6792
+ this.dir = params.dir;
6793
+ this.llmOverride = params.llmOverride;
6794
+ this.onchain = params.onchain;
6795
+ this.inner = new OnchainCallSkill({
6796
+ name: params.name,
6797
+ description: params.description,
6798
+ capabilities: params.capabilities,
6799
+ priceSubunits: BigInt(Math.round(params.priceSubunits)),
6800
+ asset: params.asset,
6801
+ scriptPath: params.scriptPath,
6802
+ scriptArgs: params.scriptArgs,
6803
+ scriptTimeoutMs: params.scriptTimeoutMs,
6804
+ scriptEnv: params.scriptEnv,
6805
+ image: params.image,
6806
+ imageFile: params.imageFile,
6807
+ llmOverride: params.llmOverride,
6808
+ onchain: params.onchain,
6809
+ network: params.network
6810
+ });
6811
+ }
6812
+ async execute(input, ctx) {
6813
+ return this.inner.execute(input, ctx);
6814
+ }
6815
+ };
6707
6816
  var ScriptSkill = class {
6708
6817
  name;
6709
6818
  description;
@@ -6836,7 +6945,7 @@ function scopeLlmKeys(scriptEnv, declaredProvider) {
6836
6945
  }
6837
6946
  return env;
6838
6947
  }
6839
- function buildCliSkill(parsed, entryPath, scriptEnv) {
6948
+ function buildCliSkill(parsed, entryPath, scriptEnv, network) {
6840
6949
  let safeImageFile = parsed.imageFile;
6841
6950
  if (safeImageFile !== void 0 && resolveInsidePathReal(entryPath, safeImageFile) === null) {
6842
6951
  console.warn(
@@ -6924,6 +7033,35 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
6924
7033
  }) : new StaticScriptSkill(scriptParams);
6925
7034
  break;
6926
7035
  }
7036
+ case "onchain": {
7037
+ if (parsed.script === void 0 || parsed.onchain === void 0) {
7038
+ throw new Error(
7039
+ `SKILL.md "${parsed.name}": internal error - script or onchain block missing for mode 'onchain'`
7040
+ );
7041
+ }
7042
+ const scriptPath = resolveInsidePathReal(entryPath, parsed.script);
7043
+ if (!scriptPath) {
7044
+ throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
7045
+ }
7046
+ skill = new OnchainSkill({
7047
+ name: parsed.name,
7048
+ description: parsed.description,
7049
+ capabilities: parsed.capabilities,
7050
+ priceSubunits: Number(parsed.priceSubunits),
7051
+ asset: parsed.asset,
7052
+ scriptPath,
7053
+ scriptArgs: parsed.scriptArgs,
7054
+ scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
7055
+ scriptEnv: scopeLlmKeys(scriptEnv, parsed.llmOverride?.provider),
7056
+ image: parsed.image,
7057
+ imageFile: safeImageFile,
7058
+ dir: entryPath,
7059
+ llmOverride: parsed.llmOverride,
7060
+ onchain: parsed.onchain,
7061
+ network
7062
+ });
7063
+ break;
7064
+ }
6927
7065
  case "x402": {
6928
7066
  if (parsed.x402 === void 0) {
6929
7067
  throw new Error(
@@ -6951,6 +7089,9 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
6951
7089
  if (parsed.executionTimeoutSecs !== void 0) {
6952
7090
  skill.executionTimeoutSecs = parsed.executionTimeoutSecs;
6953
7091
  }
7092
+ if (parsed.meteredMinSubunits !== void 0) {
7093
+ skill.meteredMinSubunits = parsed.meteredMinSubunits;
7094
+ }
6954
7095
  if (parsed.delegation) {
6955
7096
  skill.delegation = parsed.delegation;
6956
7097
  }
@@ -6989,7 +7130,7 @@ function loadSkillsFromDir(skillsDir, options) {
6989
7130
  warn: (_obj, msg) => console.warn(` ! ${msg ?? ""}`)
6990
7131
  }
6991
7132
  });
6992
- skills.push(buildCliSkill(parsed, entryPath, options.scriptEnv));
7133
+ skills.push(buildCliSkill(parsed, entryPath, options.scriptEnv, options.network));
6993
7134
  } catch (e) {
6994
7135
  const message = e instanceof Error ? e.message : String(e);
6995
7136
  console.warn(` ! Skipping skill "${entry}": ${message}`);
@@ -8368,6 +8509,12 @@ async function cmdStart(nameArg, options = {}) {
8368
8509
  agentName,
8369
8510
  agentDescription: loaded.yaml.description ?? ""
8370
8511
  };
8512
+ const onchainSkillCount = allSkills.filter((skill) => skill.onchain !== void 0).length;
8513
+ if (solanaAddress === void 0 && onchainSkillCount > 0) {
8514
+ console.warn(
8515
+ ` ! ${onchainSkillCount} onchain capability(ies) declare a block but this agent has no Solana payment address, so the network their cards would promise is a guess. Add a payments entry to elisym.yaml.`
8516
+ );
8517
+ }
8371
8518
  const x402Skills = allSkills.filter((skill) => skill.mode === "x402");
8372
8519
  let x402InvariantBroken;
8373
8520
  if (x402Skills.length > 0) {
@@ -8377,9 +8524,13 @@ async function cmdStart(nameArg, options = {}) {
8377
8524
  } else if (!solanaAddress) {
8378
8525
  x402InvariantBroken = "no payments[] entry in elisym.yaml";
8379
8526
  } else {
8380
- const bridgeSigner = await signerFromSecretKeyBase58(solanaSecretKey);
8381
- if (bridgeSigner.address !== solanaAddress) {
8382
- x402InvariantBroken = `payments[].address (${solanaAddress}) differs from the solana_secret_key address (${bridgeSigner.address})`;
8527
+ try {
8528
+ const bridgeSigner = await signerFromSecretKeyBase58(solanaSecretKey);
8529
+ if (bridgeSigner.address !== solanaAddress) {
8530
+ x402InvariantBroken = `payments[].address (${solanaAddress}) differs from the solana_secret_key address (${bridgeSigner.address})`;
8531
+ }
8532
+ } catch (error) {
8533
+ x402InvariantBroken = `solana_secret_key is present but unreadable - ${error instanceof Error ? error.message : String(error)}`;
8383
8534
  }
8384
8535
  }
8385
8536
  for (const skill of x402Skills) {
@@ -8655,6 +8806,16 @@ async function cmdStart(nameArg, options = {}) {
8655
8806
  );
8656
8807
  continue;
8657
8808
  }
8809
+ if (withholdsOnchainCard(skill, solanaAddress)) {
8810
+ console.warn(
8811
+ ` ! Not republishing "${skill.name}" - no Solana payments entry. An earlier card for it, if any, stays on the relays until it expires.`
8812
+ );
8813
+ logger.warn(
8814
+ { event: "publish_skipped_onchain_no_wallet", kind: 31990, skill: skill.name },
8815
+ "capability not advertised - onchain block with no Solana payment address"
8816
+ );
8817
+ continue;
8818
+ }
8658
8819
  try {
8659
8820
  await client.discovery.publishCapability(identity2, buildCard(skill), kinds);
8660
8821
  cardsPublished += 1;
@@ -8832,6 +8993,26 @@ function buildCapabilityCard(skill, inputs) {
8832
8993
  // exists (delegatePubkey resolved). The pubkey is injected here, never
8833
8994
  // taken from the SKILL.md - the operator declares only the cap/mechanism.
8834
8995
  ...skill.delegation && delegatePubkey ? { delegation: { ...skill.delegation, delegate_pubkey: delegatePubkey } } : {},
8996
+ // Metered descriptor. The gate is COMPOUND on purpose and both halves
8997
+ // matter: `delegatePubkey` is agent-wide (hoisted when ANY skill declares
8998
+ // delegation), while `skill.delegation` is per-skill. Stamping on the agent
8999
+ // half alone would advertise pay-per-use on a card that ships no delegation
9000
+ // block - the buyer would be told "you pay for what you use" while the only
9001
+ // rail they can reach charges the ceiling. The loader already refuses
9002
+ // `metered` without `delegation`, so this is defence in depth.
9003
+ // `solanaAddress` is in the gate too: without it the `payment` block below
9004
+ // is omitted, and a metered descriptor with no price to clamp against is
9005
+ // incoherent by construction - the write-side mirror would then reject the
9006
+ // card with a misleading "malformed metered descriptor" instead of the real
9007
+ // reason, which is that the agent has no payment address.
9008
+ ...skill.meteredMinSubunits !== void 0 && skill.delegation && delegatePubkey && solanaAddress ? { metered: { min_subunits: skill.meteredMinSubunits.toString() } } : {},
9009
+ // On-chain promise (`mode: onchain`): the network is stamped from the
9010
+ // agent's wallet here, never from SKILL.md, so a capability copied between
9011
+ // a devnet and a mainnet agent cannot advertise the wrong chain.
9012
+ // Only stamped when the agent actually has a Solana payment entry: without
9013
+ // one `walletNetwork` is a default, and a card must never promise a chain
9014
+ // the operator never chose. `cmdStart` warns for such a skill at step 7a.
9015
+ ...skill.onchain && solanaAddress ? { onchain: { ...skill.onchain, network: walletNetwork } } : {},
8835
9016
  payment: solanaAddress ? {
8836
9017
  chain: "solana",
8837
9018
  network: walletNetwork,
@@ -8844,6 +9025,9 @@ function buildCapabilityCard(skill, inputs) {
8844
9025
  } : void 0
8845
9026
  };
8846
9027
  }
9028
+ function withholdsOnchainCard(skill, solanaAddress) {
9029
+ return skill.onchain !== void 0 && solanaAddress === void 0;
9030
+ }
8847
9031
  function buildScriptEnv(secrets) {
8848
9032
  const scriptEnv = { ...process.env };
8849
9033
  delete scriptEnv.ELISYM_PASSPHRASE;