@elisym/cli 0.25.2 → 0.26.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 +1674 -933
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,27 +2,27 @@
|
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync, appendFileSync, unlinkSync, rmdirSync, realpathSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
|
|
5
|
-
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, signerFromSecretKeyBase58, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport,
|
|
6
|
-
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, ensureGitignoreHasSessionsEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
7
|
-
import { isAddress, createSolanaRpc, address } from '@solana/kit';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, signerFromSecretKeyBase58, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, generateSolanaWallet, verifyAgentIdentities, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, SESSION_ID_REGEX, createSlidingWindowLimiter, utf8ByteLength, readAcceptedTransports, resolveDelegationAsset, MAX_PROOF_TTL_SECS, PROOF_CLOCK_SKEW_SECS, verifyDelegationAuthProof, deriveOwnerDelegationAta, getDelegation, buildDelegatedTransfer, buildSignedPull, sendConfirmToTerminal, parseDelegatedPayment, confirmPullToTerminal, isDefinitelyUnpaid, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, GITHUB_USERNAME_REGEX, X_USERNAME_REGEX, normalizeNip05Identifier, splitNip05Identifier, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
|
|
6
|
+
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, ensureGitignoreHasSessionsEntry, ensureGitignoreHasDelegationNoncesEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
7
|
+
import { isAddress, createSolanaRpc, address, signature } from '@solana/kit';
|
|
8
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
9
|
-
import
|
|
9
|
+
import YAML2 from 'yaml';
|
|
10
10
|
import { Command } from 'commander';
|
|
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';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import Decimal from 'decimal.js-light';
|
|
15
|
+
import { decodePaymentRequiredHeader } from '@x402/core/http';
|
|
11
16
|
import { createHash } from 'node:crypto';
|
|
12
17
|
import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, ScriptExecutionError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
|
|
13
18
|
import { createIrohTransport } from '@elisym/sdk/node';
|
|
14
19
|
import { lookup } from 'node:dns/promises';
|
|
15
20
|
import { Socket } from 'node:net';
|
|
16
21
|
import pino from 'pino';
|
|
17
|
-
import { mkdir, writeFile, readFile, mkdtemp, rm, rename, stat } from 'node:fs/promises';
|
|
18
22
|
import { tmpdir } from 'node:os';
|
|
19
23
|
import pLimit from 'p-limit';
|
|
20
|
-
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';
|
|
21
24
|
import { x402Client, wrapFetchWithPayment } from '@x402/fetch';
|
|
22
25
|
import { ExactSvmScheme } from '@x402/svm';
|
|
23
|
-
import { decodePaymentRequiredHeader } from '@x402/core/http';
|
|
24
|
-
import chalk from 'chalk';
|
|
25
|
-
import Decimal from 'decimal.js-light';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
|
|
28
28
|
var __defProp = Object.defineProperty;
|
|
@@ -1102,14 +1102,14 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
1102
1102
|
if (options.config) {
|
|
1103
1103
|
const configPath = resolve(cwd, options.config);
|
|
1104
1104
|
const raw = readFileSync(configPath, "utf-8");
|
|
1105
|
-
template = ElisymYamlSchema.parse(
|
|
1105
|
+
template = ElisymYamlSchema.parse(YAML2.parse(raw) ?? {});
|
|
1106
1106
|
console.log(` Loaded template from ${configPath}
|
|
1107
1107
|
`);
|
|
1108
1108
|
} else if (options.defaults) {
|
|
1109
1109
|
template = buildDefaultYaml();
|
|
1110
1110
|
console.log(" Using default skeleton (no LLM, no payments). Edit later via `profile`.\n");
|
|
1111
1111
|
}
|
|
1112
|
-
const agentName = await
|
|
1112
|
+
const agentName = await resolveAgentName2(nameArg, inquirer);
|
|
1113
1113
|
const target = pickTarget(options);
|
|
1114
1114
|
const sameLocation = target === "home" ? resolveInHome(agentName) : resolveInProject(agentName, cwd);
|
|
1115
1115
|
if (sameLocation) {
|
|
@@ -1291,7 +1291,7 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
1291
1291
|
}
|
|
1292
1292
|
console.log();
|
|
1293
1293
|
}
|
|
1294
|
-
async function
|
|
1294
|
+
async function resolveAgentName2(nameArg, inquirer) {
|
|
1295
1295
|
if (nameArg) {
|
|
1296
1296
|
validateAgentName(nameArg);
|
|
1297
1297
|
return nameArg;
|
|
@@ -1442,103 +1442,928 @@ var init_init = __esm({
|
|
|
1442
1442
|
init_llm();
|
|
1443
1443
|
}
|
|
1444
1444
|
});
|
|
1445
|
-
var
|
|
1446
|
-
var
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
function xProofText(npub) {
|
|
1458
|
-
return `Verifying my account on nostr My Public Key: "${npub}"`;
|
|
1459
|
-
}
|
|
1460
|
-
function websiteNostrJson(local, pubkeyHex) {
|
|
1461
|
-
return JSON.stringify({ names: { [local]: pubkeyHex } }, null, 2);
|
|
1462
|
-
}
|
|
1463
|
-
function parseIdentityPlatform(raw) {
|
|
1464
|
-
const lowered = raw.trim().toLowerCase();
|
|
1465
|
-
const match = IDENTITY_PLATFORMS.find((platform) => platform === lowered);
|
|
1466
|
-
if (match === void 0) {
|
|
1467
|
-
throw new Error(`Unknown platform "${raw}". Expected github, x, or website.`);
|
|
1445
|
+
var MAX_CONCURRENT_JOBS = 10;
|
|
1446
|
+
var RECOVERY_MAX_RETRIES = 5;
|
|
1447
|
+
var RECOVERY_INTERVAL_SECS = 60;
|
|
1448
|
+
var WATCHDOG_PROBE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1449
|
+
var WATCHDOG_PROBE_TIMEOUT_MS = 1e4;
|
|
1450
|
+
var WATCHDOG_SELF_PING_INTERVAL_MS = 10 * 60 * 1e3;
|
|
1451
|
+
var WATCHDOG_SELF_PING_TIMEOUT_MS = 15e3;
|
|
1452
|
+
var WATCHDOG_SLEEP_DETECT_MULTIPLIER = 2;
|
|
1453
|
+
function getRpcUrl(_network) {
|
|
1454
|
+
const envUrl = process.env.SOLANA_RPC_URL;
|
|
1455
|
+
if (envUrl) {
|
|
1456
|
+
return envUrl;
|
|
1468
1457
|
}
|
|
1469
|
-
return
|
|
1458
|
+
return "https://api.devnet.solana.com";
|
|
1470
1459
|
}
|
|
1471
|
-
function
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
return { proofId: bareId };
|
|
1460
|
+
async function fetchUsdcBalance(rpc, owner) {
|
|
1461
|
+
const mint = USDC_SOLANA_DEVNET.mint;
|
|
1462
|
+
if (!mint) {
|
|
1463
|
+
return 0n;
|
|
1476
1464
|
}
|
|
1477
|
-
let url;
|
|
1478
1465
|
try {
|
|
1479
|
-
|
|
1466
|
+
const response = await rpc.getTokenAccountsByOwner(
|
|
1467
|
+
owner,
|
|
1468
|
+
{ mint: address(mint) },
|
|
1469
|
+
{ encoding: "jsonParsed", commitment: "confirmed" }
|
|
1470
|
+
).send();
|
|
1471
|
+
let total = 0n;
|
|
1472
|
+
for (const entry of response.value) {
|
|
1473
|
+
const parsed = entry.account.data;
|
|
1474
|
+
const raw = parsed?.parsed?.info?.tokenAmount?.amount;
|
|
1475
|
+
if (typeof raw === "string") {
|
|
1476
|
+
total += BigInt(raw);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
return total;
|
|
1480
1480
|
} catch {
|
|
1481
|
-
return
|
|
1481
|
+
return 0n;
|
|
1482
1482
|
}
|
|
1483
|
-
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// src/x402/constants.ts
|
|
1486
|
+
var X402_SOLANA_DEVNET_CAIP2 = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
1487
|
+
var X402_SOLANA_DEVNET_V1 = "solana-devnet";
|
|
1488
|
+
var X402_GET_INPUT_MAX_ENCODED_BYTES = 2048;
|
|
1489
|
+
var X402_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
1490
|
+
var X402_MAX_CHALLENGE_BYTES = 256 * 1024;
|
|
1491
|
+
var X402_PROBE_TTL_MS = 3e4;
|
|
1492
|
+
var X402_MAX_PAID_ATTEMPTS = 2;
|
|
1493
|
+
var X402_MAX_PAYMENT_SIGNATURES = 2 * X402_MAX_PAID_ATTEMPTS;
|
|
1494
|
+
var X402_FREE_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
1495
|
+
var X402_REFUNDED_RETRIES = 1;
|
|
1496
|
+
var X402_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1497
|
+
|
|
1498
|
+
// src/x402/matcher.ts
|
|
1499
|
+
function requirementAmount(requirement) {
|
|
1500
|
+
const v1Amount = requirement.maxAmountRequired;
|
|
1501
|
+
const raw = requirement.amount ?? v1Amount;
|
|
1502
|
+
if (typeof raw !== "string" || !/^[0-9]+$/.test(raw)) {
|
|
1484
1503
|
return null;
|
|
1485
1504
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1505
|
+
return BigInt(raw);
|
|
1506
|
+
}
|
|
1507
|
+
function isAcceptableRequirement(requirement, rule) {
|
|
1508
|
+
if (requirement.scheme !== "exact") {
|
|
1509
|
+
return false;
|
|
1490
1510
|
}
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
return null;
|
|
1511
|
+
const network = requirement.network;
|
|
1512
|
+
if (network !== X402_SOLANA_DEVNET_CAIP2 && network !== X402_SOLANA_DEVNET_V1) {
|
|
1513
|
+
return false;
|
|
1495
1514
|
}
|
|
1496
|
-
|
|
1515
|
+
if (USDC_SOLANA_DEVNET.mint === void 0 || requirement.asset !== USDC_SOLANA_DEVNET.mint) {
|
|
1516
|
+
return false;
|
|
1517
|
+
}
|
|
1518
|
+
const amount = requirementAmount(requirement);
|
|
1519
|
+
if (amount === null) {
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
return amount <= rule.maxUpstreamSubunits;
|
|
1497
1523
|
}
|
|
1498
|
-
function
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1524
|
+
function selectAcceptableRequirement(accepts, rule) {
|
|
1525
|
+
return accepts.find((requirement) => isAcceptableRequirement(requirement, rule));
|
|
1526
|
+
}
|
|
1527
|
+
function maxAcceptableQuote(accepts, rule) {
|
|
1528
|
+
let worst = null;
|
|
1529
|
+
for (const requirement of accepts) {
|
|
1530
|
+
if (!isAcceptableRequirement(requirement, rule)) {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
const amount = requirementAmount(requirement);
|
|
1534
|
+
if (amount === null) {
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (worst === null || amount > worst) {
|
|
1538
|
+
worst = amount;
|
|
1539
|
+
}
|
|
1502
1540
|
}
|
|
1503
|
-
|
|
1541
|
+
return worst;
|
|
1542
|
+
}
|
|
1543
|
+
function buildRequirementsPolicy(rule) {
|
|
1544
|
+
return function filterAcceptableRequirements(_x402Version, paymentRequirements) {
|
|
1545
|
+
return paymentRequirements.filter((requirement) => isAcceptableRequirement(requirement, rule));
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
1549
|
+
var MAX_PROBE_BODY_BYTES = 256 * 1024;
|
|
1550
|
+
var PROBE_CACHE_MAX_ENTRIES = 256;
|
|
1551
|
+
async function readTextCapped(response, maxBytes) {
|
|
1552
|
+
const reader = response.body?.getReader();
|
|
1553
|
+
if (reader === void 0) {
|
|
1554
|
+
const buffer = await response.arrayBuffer();
|
|
1555
|
+
if (buffer.byteLength > maxBytes) {
|
|
1556
|
+
throw new X402ProbeError(`402 body exceeds ${maxBytes} bytes`, 402);
|
|
1557
|
+
}
|
|
1558
|
+
return new TextDecoder().decode(buffer);
|
|
1559
|
+
}
|
|
1560
|
+
const chunks = [];
|
|
1561
|
+
let total = 0;
|
|
1562
|
+
while (true) {
|
|
1563
|
+
const { done, value } = await reader.read();
|
|
1564
|
+
if (done) {
|
|
1565
|
+
break;
|
|
1566
|
+
}
|
|
1567
|
+
total += value.byteLength;
|
|
1568
|
+
if (total > maxBytes) {
|
|
1569
|
+
await reader.cancel().catch(() => {
|
|
1570
|
+
});
|
|
1571
|
+
throw new X402ProbeError(`402 body exceeds ${maxBytes} bytes`, 402);
|
|
1572
|
+
}
|
|
1573
|
+
chunks.push(value);
|
|
1574
|
+
}
|
|
1575
|
+
const combined = new Uint8Array(total);
|
|
1576
|
+
let offset = 0;
|
|
1577
|
+
for (const chunk of chunks) {
|
|
1578
|
+
combined.set(chunk, offset);
|
|
1579
|
+
offset += chunk.byteLength;
|
|
1580
|
+
}
|
|
1581
|
+
return new TextDecoder().decode(combined);
|
|
1582
|
+
}
|
|
1583
|
+
var X402ProbeError = class extends Error {
|
|
1584
|
+
status;
|
|
1585
|
+
constructor(message, status) {
|
|
1586
|
+
super(message);
|
|
1587
|
+
this.name = "X402ProbeError";
|
|
1588
|
+
this.status = status;
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
function normalizeV1Requirement(raw) {
|
|
1592
|
+
return {
|
|
1593
|
+
scheme: typeof raw.scheme === "string" ? raw.scheme : "",
|
|
1594
|
+
network: typeof raw.network === "string" ? raw.network : "unknown:unknown",
|
|
1595
|
+
asset: typeof raw.asset === "string" ? raw.asset : "",
|
|
1596
|
+
payTo: typeof raw.payTo === "string" ? raw.payTo : "",
|
|
1597
|
+
amount: typeof raw.maxAmountRequired === "string" ? raw.maxAmountRequired : "",
|
|
1598
|
+
maxTimeoutSeconds: typeof raw.maxTimeoutSeconds === "number" ? raw.maxTimeoutSeconds : 60,
|
|
1599
|
+
extra: raw.extra ?? {}
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
function parseV1Body(bodyText) {
|
|
1603
|
+
let parsed;
|
|
1504
1604
|
try {
|
|
1505
|
-
|
|
1605
|
+
parsed = JSON.parse(bodyText);
|
|
1506
1606
|
} catch {
|
|
1507
1607
|
return null;
|
|
1508
1608
|
}
|
|
1509
|
-
if (
|
|
1609
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1510
1610
|
return null;
|
|
1511
1611
|
}
|
|
1512
|
-
const
|
|
1513
|
-
|
|
1514
|
-
if (userSegment === void 0 || statusLiteral !== "status" || idSegment === void 0) {
|
|
1612
|
+
const body = parsed;
|
|
1613
|
+
if (body.x402Version !== 1 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
|
|
1515
1614
|
return null;
|
|
1516
1615
|
}
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1616
|
+
const rawRequirements = body.accepts.filter(
|
|
1617
|
+
(item) => typeof item === "object" && item !== null
|
|
1618
|
+
);
|
|
1619
|
+
if (rawRequirements.length === 0) {
|
|
1519
1620
|
return null;
|
|
1520
1621
|
}
|
|
1521
|
-
|
|
1622
|
+
const accepts = rawRequirements.map(normalizeV1Requirement);
|
|
1623
|
+
const first = rawRequirements[0];
|
|
1624
|
+
return {
|
|
1625
|
+
x402Version: 1,
|
|
1626
|
+
accepts,
|
|
1627
|
+
resource: {
|
|
1628
|
+
url: typeof first.resource === "string" ? first.resource : void 0,
|
|
1629
|
+
description: typeof first.description === "string" ? first.description : void 0,
|
|
1630
|
+
mimeType: typeof first.mimeType === "string" ? first.mimeType : void 0
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1522
1633
|
}
|
|
1523
|
-
function
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
handle: identities.github.username,
|
|
1529
|
-
proofId: identities.github.gist
|
|
1634
|
+
async function probePaymentRequired(url, method, signal) {
|
|
1635
|
+
const probeSignal = AbortSignal.timeout(PROBE_TIMEOUT_MS);
|
|
1636
|
+
const response = await fetch(url, { method, signal: probeSignal, redirect: "error" });
|
|
1637
|
+
if (response.status !== 402) {
|
|
1638
|
+
await response.body?.cancel().catch(() => {
|
|
1530
1639
|
});
|
|
1640
|
+
throw new X402ProbeError(
|
|
1641
|
+
`expected HTTP 402 from ${url}, got ${response.status} - not an x402-paid endpoint (or it validates the request before the paywall; try the other method)`,
|
|
1642
|
+
response.status
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const headerValue = response.headers.get("PAYMENT-REQUIRED");
|
|
1646
|
+
if (headerValue !== null) {
|
|
1647
|
+
await response.body?.cancel().catch(() => {
|
|
1648
|
+
});
|
|
1649
|
+
try {
|
|
1650
|
+
const paymentRequired = decodePaymentRequiredHeader(headerValue);
|
|
1651
|
+
return {
|
|
1652
|
+
x402Version: paymentRequired.x402Version,
|
|
1653
|
+
accepts: paymentRequired.accepts,
|
|
1654
|
+
resource: paymentRequired.resource
|
|
1655
|
+
};
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1658
|
+
throw new X402ProbeError(`could not decode PAYMENT-REQUIRED header: ${message}`, 402);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const bodyText = await readTextCapped(response, MAX_PROBE_BODY_BYTES).catch((error) => {
|
|
1662
|
+
if (error instanceof X402ProbeError) {
|
|
1663
|
+
throw error;
|
|
1664
|
+
}
|
|
1665
|
+
return "";
|
|
1666
|
+
});
|
|
1667
|
+
const v1 = parseV1Body(bodyText);
|
|
1668
|
+
if (v1 !== null) {
|
|
1669
|
+
return v1;
|
|
1670
|
+
}
|
|
1671
|
+
throw new X402ProbeError(
|
|
1672
|
+
"got HTTP 402 but neither a v2 PAYMENT-REQUIRED header nor a v1 JSON body was parseable",
|
|
1673
|
+
402
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
var PROBE_CACHE = /* @__PURE__ */ new Map();
|
|
1677
|
+
async function probePaymentRequiredCached(url, method, ttlMs) {
|
|
1678
|
+
const key = `${method} ${url}`;
|
|
1679
|
+
const cached = PROBE_CACHE.get(key);
|
|
1680
|
+
const now = Date.now();
|
|
1681
|
+
if (cached !== void 0 && now - cached.at < ttlMs) {
|
|
1682
|
+
return cached.result;
|
|
1683
|
+
}
|
|
1684
|
+
const result = await probePaymentRequired(url, method);
|
|
1685
|
+
PROBE_CACHE.delete(key);
|
|
1686
|
+
PROBE_CACHE.set(key, { at: now, result });
|
|
1687
|
+
while (PROBE_CACHE.size > PROBE_CACHE_MAX_ENTRIES) {
|
|
1688
|
+
const oldest = PROBE_CACHE.keys().next().value;
|
|
1689
|
+
if (oldest === void 0) {
|
|
1690
|
+
break;
|
|
1691
|
+
}
|
|
1692
|
+
PROBE_CACHE.delete(oldest);
|
|
1693
|
+
}
|
|
1694
|
+
return result;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/commands/x402-add.ts
|
|
1698
|
+
var BPS_DENOMINATOR = 1e4;
|
|
1699
|
+
var DEFAULT_MARGIN_BPS = 1e3;
|
|
1700
|
+
var SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
1701
|
+
var CIRCLE_FAUCET_URL = "https://faucet.circle.com";
|
|
1702
|
+
function fail(message) {
|
|
1703
|
+
console.error(`
|
|
1704
|
+
! ${message}
|
|
1705
|
+
`);
|
|
1706
|
+
process.exit(1);
|
|
1707
|
+
}
|
|
1708
|
+
function sanitizeUpstreamText(raw, maxLength) {
|
|
1709
|
+
let withoutControls = "";
|
|
1710
|
+
for (const char of raw) {
|
|
1711
|
+
const code = char.codePointAt(0) ?? 0;
|
|
1712
|
+
withoutControls += code < 32 || code === 127 ? " " : char;
|
|
1713
|
+
}
|
|
1714
|
+
return withoutControls.replace(/—/g, "-").replace(/\s+/g, " ").trim().slice(0, maxLength).trim();
|
|
1715
|
+
}
|
|
1716
|
+
function slugFromUrl(url) {
|
|
1717
|
+
const pathPart = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? "";
|
|
1718
|
+
const host = url.hostname.replace(/^www\./, "").split(".")[0] ?? "x402";
|
|
1719
|
+
return pathPart.length > 0 ? `${host}-${pathPart}` : host;
|
|
1720
|
+
}
|
|
1721
|
+
function parseMarginBps(raw) {
|
|
1722
|
+
if (raw === void 0) {
|
|
1723
|
+
return DEFAULT_MARGIN_BPS;
|
|
1724
|
+
}
|
|
1725
|
+
const value = Number(raw);
|
|
1726
|
+
if (!Number.isInteger(value) || value < 0 || value > 1e5) {
|
|
1727
|
+
fail(`--margin-bps must be a non-negative integer (bps, 100 = 1%); got "${raw}"`);
|
|
1728
|
+
}
|
|
1729
|
+
return value;
|
|
1730
|
+
}
|
|
1731
|
+
async function parseImportedSecret(input) {
|
|
1732
|
+
const trimmed = input.trim();
|
|
1733
|
+
if (existsSync(trimmed)) {
|
|
1734
|
+
const content = await readFile(trimmed, "utf-8");
|
|
1735
|
+
let parsed;
|
|
1736
|
+
try {
|
|
1737
|
+
parsed = JSON.parse(content);
|
|
1738
|
+
} catch {
|
|
1739
|
+
throw new Error(`${trimmed} is not a valid solana-keygen JSON file`);
|
|
1740
|
+
}
|
|
1741
|
+
if (!Array.isArray(parsed) || parsed.length !== 64 || parsed.some(
|
|
1742
|
+
(byte) => typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255
|
|
1743
|
+
)) {
|
|
1744
|
+
throw new Error(`${trimmed} must contain a JSON array of 64 bytes (solana-keygen format)`);
|
|
1745
|
+
}
|
|
1746
|
+
return encodeSecretKeyBase58(new Uint8Array(parsed));
|
|
1747
|
+
}
|
|
1748
|
+
try {
|
|
1749
|
+
await signerFromSecretKeyBase58(trimmed);
|
|
1750
|
+
} catch {
|
|
1751
|
+
throw new Error("not a readable file and not a valid base58-encoded 64-byte secret key");
|
|
1752
|
+
}
|
|
1753
|
+
return trimmed;
|
|
1754
|
+
}
|
|
1755
|
+
function computeBridgePriceSubunits(quote, marginBps, feeBps) {
|
|
1756
|
+
if (feeBps >= BPS_DENOMINATOR) {
|
|
1757
|
+
throw new Error(`feeBps ${feeBps} >= ${BPS_DENOMINATOR} - refusing zero/negative denominator`);
|
|
1758
|
+
}
|
|
1759
|
+
return BigInt(
|
|
1760
|
+
new Decimal(quote.toString()).mul(BPS_DENOMINATOR + marginBps).div(BPS_DENOMINATOR - feeBps).toDecimalPlaces(0, Decimal.ROUND_CEIL).toString()
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
function buildSkillMd(input) {
|
|
1764
|
+
const frontmatter = {
|
|
1765
|
+
name: input.skillName,
|
|
1766
|
+
description: input.description,
|
|
1767
|
+
capabilities: input.capabilities,
|
|
1768
|
+
price: input.priceDisplay,
|
|
1769
|
+
token: "usdc",
|
|
1770
|
+
mode: "x402",
|
|
1771
|
+
x402_url: input.url,
|
|
1772
|
+
x402_method: input.method,
|
|
1773
|
+
...input.queryParam !== void 0 ? { x402_query_param: input.queryParam } : {},
|
|
1774
|
+
x402_max_upstream: Number(input.quote),
|
|
1775
|
+
// GET input is dominated by the ~2KB percent-encoded query-string limit,
|
|
1776
|
+
// so the byte cap is written for POST bridges only.
|
|
1777
|
+
...input.method === "POST" ? { x402_max_input_bytes: DEFAULT_X402_MAX_INPUT_BYTES } : {}
|
|
1778
|
+
};
|
|
1779
|
+
const bodyLines = [
|
|
1780
|
+
`# ${input.skillName}`,
|
|
1781
|
+
"",
|
|
1782
|
+
"Operator notes for this x402 bridge skill (the executor ignores this body):",
|
|
1783
|
+
"",
|
|
1784
|
+
`- Upstream: ${input.url} (${input.method})`,
|
|
1785
|
+
`- Upstream quote at generation time: ${formatAssetAmount(USDC_SOLANA_DEVNET, input.quote)} (= x402_max_upstream, the signing ceiling)`,
|
|
1786
|
+
`- Price: ${input.priceDisplay} USDC (margin ${input.marginPercent}%, protocol fee ${input.feePercent}% at generation time)`,
|
|
1787
|
+
"- To reprice after an upstream change, re-run:",
|
|
1788
|
+
"",
|
|
1789
|
+
"```bash",
|
|
1790
|
+
`npx @elisym/cli x402 add ${input.url}`,
|
|
1791
|
+
"```"
|
|
1792
|
+
];
|
|
1793
|
+
return `---
|
|
1794
|
+
${YAML2.stringify(frontmatter)}---
|
|
1795
|
+
|
|
1796
|
+
${bodyLines.join("\n")}
|
|
1797
|
+
`;
|
|
1798
|
+
}
|
|
1799
|
+
function scanExistingSkills(skillsDir, candidateDTag, url) {
|
|
1800
|
+
const scan = {};
|
|
1801
|
+
let entries;
|
|
1802
|
+
try {
|
|
1803
|
+
entries = readdirSync(skillsDir);
|
|
1804
|
+
} catch {
|
|
1805
|
+
return scan;
|
|
1806
|
+
}
|
|
1807
|
+
for (const entry of entries) {
|
|
1808
|
+
const skillMdPath = join(skillsDir, entry, "SKILL.md");
|
|
1809
|
+
try {
|
|
1810
|
+
if (!statSync(join(skillsDir, entry)).isDirectory()) {
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
const { frontmatter } = parseSkillMd(readFileSync(skillMdPath, "utf-8"));
|
|
1814
|
+
if (typeof frontmatter.name !== "string") {
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
if (typeof frontmatter.x402_url === "string" && frontmatter.x402_url === url) {
|
|
1818
|
+
scan.sameUrlDir = join(skillsDir, entry);
|
|
1819
|
+
scan.sameUrlName = frontmatter.name;
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
let existingDTag;
|
|
1823
|
+
try {
|
|
1824
|
+
existingDTag = toDTag(frontmatter.name);
|
|
1825
|
+
} catch {
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
if (existingDTag === candidateDTag) {
|
|
1829
|
+
scan.dTagCollision = frontmatter.name;
|
|
1830
|
+
}
|
|
1831
|
+
} catch {
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
return scan;
|
|
1836
|
+
}
|
|
1837
|
+
function describeUnacceptableAccepts(probe) {
|
|
1838
|
+
const networks = [...new Set(probe.accepts.map((requirement) => String(requirement.network)))];
|
|
1839
|
+
if (networks.length > 0 && networks.every((network) => network.startsWith("eip155:"))) {
|
|
1840
|
+
return `the service accepts only EVM networks (${networks.join(", ")}) - not supported yet`;
|
|
1841
|
+
}
|
|
1842
|
+
if (networks.some((network) => network === SOLANA_MAINNET_CAIP2 || network === "solana")) {
|
|
1843
|
+
return "the service settles on Solana mainnet - elisym currently runs on devnet (mainnet is on the roadmap)";
|
|
1844
|
+
}
|
|
1845
|
+
return `no exact-scheme devnet-USDC requirement found (service accepts: ${networks.join(", ") || "nothing parseable"})`;
|
|
1846
|
+
}
|
|
1847
|
+
async function cmdX402Add(url, agentName, options) {
|
|
1848
|
+
const cwd = process.cwd();
|
|
1849
|
+
const { default: inquirer } = await import('inquirer');
|
|
1850
|
+
let parsedUrl;
|
|
1851
|
+
try {
|
|
1852
|
+
parsedUrl = new URL(url);
|
|
1853
|
+
} catch {
|
|
1854
|
+
fail(`"${url}" is not a valid URL`);
|
|
1855
|
+
}
|
|
1856
|
+
const isLoopback = ["localhost", "127.0.0.1", "[::1]"].includes(parsedUrl.hostname);
|
|
1857
|
+
if (parsedUrl.protocol !== "https:" && !(parsedUrl.protocol === "http:" && isLoopback)) {
|
|
1858
|
+
fail("the upstream must be an https:// URL (plain http is allowed for localhost only)");
|
|
1859
|
+
}
|
|
1860
|
+
const methodRaw = (options.method ?? "POST").toUpperCase();
|
|
1861
|
+
if (methodRaw !== "GET" && methodRaw !== "POST") {
|
|
1862
|
+
fail(`--method must be GET or POST; got "${options.method}"`);
|
|
1863
|
+
}
|
|
1864
|
+
const method = methodRaw;
|
|
1865
|
+
const marginBps = parseMarginBps(options.marginBps);
|
|
1866
|
+
if (!agentName) {
|
|
1867
|
+
const agents = await listAgents(cwd);
|
|
1868
|
+
if (agents.length === 0) {
|
|
1869
|
+
fail("no agents found - create one first: npx @elisym/cli init");
|
|
1870
|
+
}
|
|
1871
|
+
if (options.yes) {
|
|
1872
|
+
fail("--yes needs an explicit agent argument: elisym x402 add <url> <agent> --yes");
|
|
1873
|
+
}
|
|
1874
|
+
const { selected } = await inquirer.prompt([
|
|
1875
|
+
{
|
|
1876
|
+
type: "list",
|
|
1877
|
+
name: "selected",
|
|
1878
|
+
message: "Add the bridge skill to which agent?",
|
|
1879
|
+
choices: agents.map((agent) => ({
|
|
1880
|
+
name: `${agent.name} (${agent.source})`,
|
|
1881
|
+
value: agent.name
|
|
1882
|
+
}))
|
|
1883
|
+
}
|
|
1884
|
+
]);
|
|
1885
|
+
agentName = selected;
|
|
1886
|
+
}
|
|
1887
|
+
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
1888
|
+
const loaded = await loadAgent(agentName, cwd, passphrase);
|
|
1889
|
+
console.log(`
|
|
1890
|
+
Probing ${url} (${method}, unpaid)...`);
|
|
1891
|
+
let probe;
|
|
1892
|
+
try {
|
|
1893
|
+
probe = await probePaymentRequired(url, method);
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
if (error instanceof X402ProbeError) {
|
|
1896
|
+
fail(error.message);
|
|
1897
|
+
}
|
|
1898
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1899
|
+
fail(`upstream unreachable: ${message}`);
|
|
1900
|
+
}
|
|
1901
|
+
const anyCeiling = { maxUpstreamSubunits: BigInt(Number.MAX_SAFE_INTEGER) };
|
|
1902
|
+
const requirement = selectAcceptableRequirement(probe.accepts, anyCeiling);
|
|
1903
|
+
if (requirement === void 0) {
|
|
1904
|
+
fail(describeUnacceptableAccepts(probe));
|
|
1905
|
+
}
|
|
1906
|
+
const quote = requirementAmount(requirement);
|
|
1907
|
+
if (quote === null || quote <= 0n) {
|
|
1908
|
+
fail("the upstream quote is not a positive integer amount");
|
|
1909
|
+
}
|
|
1910
|
+
let solanaSecretKey = loaded.secrets.solana_secret_key;
|
|
1911
|
+
if (solanaSecretKey === void 0 || solanaSecretKey.length === 0) {
|
|
1912
|
+
let choice;
|
|
1913
|
+
if (options.yes || options.generateWallet) {
|
|
1914
|
+
if (!options.generateWallet) {
|
|
1915
|
+
fail(
|
|
1916
|
+
"this agent has no Solana wallet key. Run interactively to generate/import one, or pass --generate-wallet (a money-holding key is never created silently under --yes)."
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
choice = "generate";
|
|
1920
|
+
} else {
|
|
1921
|
+
const { walletChoice } = await inquirer.prompt([
|
|
1922
|
+
{
|
|
1923
|
+
type: "list",
|
|
1924
|
+
name: "walletChoice",
|
|
1925
|
+
message: "The bridge pays the upstream from the agent wallet, but .secrets.json has no solana_secret_key:",
|
|
1926
|
+
choices: [
|
|
1927
|
+
{ name: "Generate a new wallet", value: "generate" },
|
|
1928
|
+
{ name: "Import an existing one (solana-keygen JSON file or base58)", value: "import" }
|
|
1929
|
+
]
|
|
1930
|
+
}
|
|
1931
|
+
]);
|
|
1932
|
+
choice = walletChoice;
|
|
1933
|
+
}
|
|
1934
|
+
if (choice === "generate") {
|
|
1935
|
+
const wallet = await generateSolanaWallet();
|
|
1936
|
+
solanaSecretKey = wallet.secretKeyBase58;
|
|
1937
|
+
} else {
|
|
1938
|
+
const { imported } = await inquirer.prompt([
|
|
1939
|
+
{
|
|
1940
|
+
// Masked: a pasted base58 secret key must not echo to the terminal
|
|
1941
|
+
// scrollback (matches every other secret prompt in the CLI). A file
|
|
1942
|
+
// path can still be pasted/drag-dropped under the mask.
|
|
1943
|
+
type: "password",
|
|
1944
|
+
mask: "*",
|
|
1945
|
+
name: "imported",
|
|
1946
|
+
message: "Path to a solana-keygen JSON file (or paste a base58 secret key):"
|
|
1947
|
+
}
|
|
1948
|
+
]);
|
|
1949
|
+
try {
|
|
1950
|
+
solanaSecretKey = await parseImportedSecret(imported);
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
if (loaded.secrets.solana_delegate_secret_key) {
|
|
1956
|
+
const paymentSigner = await signerFromSecretKeyBase58(solanaSecretKey);
|
|
1957
|
+
const delegateSigner = await signerFromSecretKeyBase58(
|
|
1958
|
+
loaded.secrets.solana_delegate_secret_key
|
|
1959
|
+
);
|
|
1960
|
+
if (paymentSigner.address === delegateSigner.address) {
|
|
1961
|
+
fail(
|
|
1962
|
+
"the wallet key equals this agent delegate key (solana_delegate_secret_key). They must differ for blast-radius isolation - import a different wallet key or rotate the delegate key."
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
await writeSecrets(
|
|
1967
|
+
loaded.dir,
|
|
1968
|
+
{ ...loaded.secrets, solana_secret_key: solanaSecretKey },
|
|
1969
|
+
passphrase
|
|
1970
|
+
);
|
|
1971
|
+
console.log(" Wallet key saved to .secrets.json");
|
|
1972
|
+
}
|
|
1973
|
+
const signer = await signerFromSecretKeyBase58(solanaSecretKey);
|
|
1974
|
+
const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
|
|
1975
|
+
if (solPayment !== void 0 && solPayment.address !== signer.address) {
|
|
1976
|
+
fail(
|
|
1977
|
+
`wallet invariant violated: elisym.yaml payments[].address (${solPayment.address}) differs from the solana_secret_key address (${signer.address}).
|
|
1978
|
+
Revenue must land in the wallet the bridge spends from, or the float never refills.
|
|
1979
|
+
Fix: set payments[].address to the key address, or import the matching key.`
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
if (solPayment === void 0) {
|
|
1983
|
+
console.log(
|
|
1984
|
+
` elisym.yaml has no Solana payments entry; adding address ${signer.address} (devnet).`
|
|
1985
|
+
);
|
|
1986
|
+
await writeYaml(loaded.dir, {
|
|
1987
|
+
...loaded.yaml,
|
|
1988
|
+
payments: [
|
|
1989
|
+
...loaded.yaml.payments,
|
|
1990
|
+
{ chain: "solana", network: "devnet", address: signer.address }
|
|
1991
|
+
]
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
const rpc = createSolanaRpc(getRpcUrl());
|
|
1995
|
+
const balance = await fetchUsdcBalance(rpc, address(signer.address));
|
|
1996
|
+
let feeBps;
|
|
1997
|
+
try {
|
|
1998
|
+
const protocolConfig = await getProtocolConfig(rpc, getProtocolProgramId("devnet"), {
|
|
1999
|
+
forceRefresh: true
|
|
2000
|
+
});
|
|
2001
|
+
feeBps = protocolConfig.feeBps;
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2004
|
+
fail(`could not read the on-chain protocol config (fee): ${message}`);
|
|
2005
|
+
}
|
|
2006
|
+
if (feeBps >= BPS_DENOMINATOR) {
|
|
2007
|
+
fail(
|
|
2008
|
+
`protocol fee is ${feeBps} bps (>= 100%) - price math would divide by zero or go negative`
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
const priceSubunits = computeBridgePriceSubunits(quote, marginBps, feeBps);
|
|
2012
|
+
const priceDisplay = new Decimal(priceSubunits.toString()).div(new Decimal(10).pow(USDC_SOLANA_DEVNET.decimals)).toString();
|
|
2013
|
+
const protocolFee = calculateProtocolFee(Number(priceSubunits), feeBps);
|
|
2014
|
+
const netMargin = priceSubunits - BigInt(protocolFee) - quote;
|
|
2015
|
+
const fallbackName = slugFromUrl(parsedUrl);
|
|
2016
|
+
const rawName = options.name ?? probe.resource?.serviceName ?? fallbackName;
|
|
2017
|
+
const skillName = sanitizeUpstreamText(rawName, LIMITS.MAX_AGENT_NAME_LENGTH);
|
|
2018
|
+
if (!/[a-zA-Z0-9]/.test(skillName)) {
|
|
2019
|
+
fail(
|
|
2020
|
+
`the upstream service name ("${rawName}") has no ASCII letters or digits - pass an explicit --name <skill-name>`
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
const candidateDTag = toDTag(skillName);
|
|
2024
|
+
const description = sanitizeUpstreamText(
|
|
2025
|
+
probe.resource?.description ?? `x402 bridge to ${parsedUrl.hostname}`,
|
|
2026
|
+
LIMITS.MAX_DESCRIPTION_LENGTH
|
|
2027
|
+
);
|
|
2028
|
+
const capabilities = [candidateDTag];
|
|
2029
|
+
const skillsDir = join(loaded.dir, "skills");
|
|
2030
|
+
const scan = scanExistingSkills(skillsDir, candidateDTag, url);
|
|
2031
|
+
let targetDir = join(skillsDir, candidateDTag);
|
|
2032
|
+
if (scan.sameUrlDir !== void 0) {
|
|
2033
|
+
if (!options.yes) {
|
|
2034
|
+
const { update } = await inquirer.prompt([
|
|
2035
|
+
{
|
|
2036
|
+
type: "confirm",
|
|
2037
|
+
name: "update",
|
|
2038
|
+
message: `Skill "${scan.sameUrlName}" already bridges this URL - regenerate it with the fresh quote?`,
|
|
2039
|
+
default: true
|
|
2040
|
+
}
|
|
2041
|
+
]);
|
|
2042
|
+
if (!update) {
|
|
2043
|
+
fail("aborted");
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
targetDir = scan.sameUrlDir;
|
|
2047
|
+
} else if (scan.dTagCollision !== void 0) {
|
|
2048
|
+
fail(
|
|
2049
|
+
`the generated name "${skillName}" collides with existing skill "${scan.dTagCollision}" (both map to d-tag "${candidateDTag}", which would replace its live discovery card) - pass a distinct --name`
|
|
2050
|
+
);
|
|
2051
|
+
} else if (existsSync(targetDir)) {
|
|
2052
|
+
fail(`skill directory ${targetDir} already exists - pass a distinct --name`);
|
|
2053
|
+
}
|
|
2054
|
+
let queryParam = options.queryParam;
|
|
2055
|
+
if (method === "GET" && queryParam === void 0 && !options.yes) {
|
|
2056
|
+
const { param } = await inquirer.prompt([
|
|
2057
|
+
{
|
|
2058
|
+
type: "input",
|
|
2059
|
+
name: "param",
|
|
2060
|
+
message: "GET upstream: which query parameter should carry the buyer input? (empty = the skill takes no input)"
|
|
2061
|
+
}
|
|
2062
|
+
]);
|
|
2063
|
+
queryParam = param.trim() === "" ? void 0 : param.trim();
|
|
2064
|
+
}
|
|
2065
|
+
if (queryParam !== void 0 && parsedUrl.searchParams.has(queryParam)) {
|
|
2066
|
+
fail(`--query-param "${queryParam}" collides with a parameter already present in the URL`);
|
|
2067
|
+
}
|
|
2068
|
+
if (method === "POST" && queryParam !== void 0) {
|
|
2069
|
+
fail("--query-param only applies to GET upstreams (POST maps the input to the request body)");
|
|
2070
|
+
}
|
|
2071
|
+
const noInput = method === "GET" && queryParam === void 0;
|
|
2072
|
+
const feePercent = new Decimal(feeBps).div(100).toString();
|
|
2073
|
+
const marginPercent = new Decimal(marginBps).div(100).toString();
|
|
2074
|
+
console.log("");
|
|
2075
|
+
console.log(` ${chalk.bold(skillName)}`);
|
|
2076
|
+
console.log(` ${description}`);
|
|
2077
|
+
console.log("");
|
|
2078
|
+
console.log(
|
|
2079
|
+
` Upstream ${url} (${method}${queryParam !== void 0 ? `, input in ?${queryParam}=` : ""}${noInput ? ", no buyer input" : ""})`
|
|
2080
|
+
);
|
|
2081
|
+
console.log(` Quote ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)}`);
|
|
2082
|
+
console.log(
|
|
2083
|
+
` Your price ${formatAssetAmount(USDC_SOLANA_DEVNET, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
|
|
2084
|
+
);
|
|
2085
|
+
console.log(` Net margin ${formatAssetAmount(USDC_SOLANA_DEVNET, netMargin)} per job`);
|
|
2086
|
+
console.log(
|
|
2087
|
+
` Float ${formatAssetAmount(USDC_SOLANA_DEVNET, balance)} in ${signer.address}`
|
|
2088
|
+
);
|
|
2089
|
+
if (balance < quote) {
|
|
2090
|
+
console.log(
|
|
2091
|
+
chalk.yellow(
|
|
2092
|
+
` ! Float below one upstream quote - fund the wallet with devnet USDC: ${CIRCLE_FAUCET_URL}`
|
|
2093
|
+
)
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
console.log(` Skill dir ${targetDir}`);
|
|
2097
|
+
console.log("");
|
|
2098
|
+
if (!options.yes) {
|
|
2099
|
+
const { confirmed } = await inquirer.prompt([
|
|
2100
|
+
{ type: "confirm", name: "confirmed", message: "Write the skill?", default: true }
|
|
2101
|
+
]);
|
|
2102
|
+
if (!confirmed) {
|
|
2103
|
+
fail("aborted");
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
const content = buildSkillMd({
|
|
2107
|
+
skillName,
|
|
2108
|
+
description,
|
|
2109
|
+
capabilities,
|
|
2110
|
+
priceDisplay,
|
|
2111
|
+
url,
|
|
2112
|
+
method,
|
|
2113
|
+
queryParam,
|
|
2114
|
+
quote,
|
|
2115
|
+
marginPercent,
|
|
2116
|
+
feePercent
|
|
2117
|
+
});
|
|
2118
|
+
const roundTrip = parseSkillMd(content);
|
|
2119
|
+
validateSkillFrontmatter(roundTrip.frontmatter, roundTrip.systemPrompt, {
|
|
2120
|
+
allowFreeSkills: false,
|
|
2121
|
+
allowX402Skills: true
|
|
2122
|
+
});
|
|
2123
|
+
await mkdir(targetDir, { recursive: true });
|
|
2124
|
+
await writeFile(join(targetDir, "SKILL.md"), content, "utf-8");
|
|
2125
|
+
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
2126
|
+
console.log(`
|
|
2127
|
+
Wrote ${join(targetDir, "SKILL.md")}`);
|
|
2128
|
+
if (balance < quote) {
|
|
2129
|
+
console.log(` Next: fund the float (devnet USDC): ${CIRCLE_FAUCET_URL}`);
|
|
2130
|
+
}
|
|
2131
|
+
console.log(` Start the agent: npx @elisym/cli start ${agentName}
|
|
2132
|
+
`);
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// src/commands/delegate-key.ts
|
|
2136
|
+
async function resolveAgentName(name, cwd, nonInteractive) {
|
|
2137
|
+
if (name) {
|
|
2138
|
+
return name;
|
|
2139
|
+
}
|
|
2140
|
+
if (nonInteractive) {
|
|
2141
|
+
console.error("--yes needs an explicit agent argument: elisym delegate-key <agent> --yes");
|
|
2142
|
+
process.exit(1);
|
|
2143
|
+
}
|
|
2144
|
+
const agents = await listAgents(cwd);
|
|
2145
|
+
if (agents.length === 0) {
|
|
2146
|
+
console.error("No agents found.");
|
|
2147
|
+
process.exit(1);
|
|
2148
|
+
}
|
|
2149
|
+
const { default: inquirer } = await import('inquirer');
|
|
2150
|
+
const { selected } = await inquirer.prompt([
|
|
2151
|
+
{
|
|
2152
|
+
type: "list",
|
|
2153
|
+
name: "selected",
|
|
2154
|
+
message: "Select agent:",
|
|
2155
|
+
choices: agents.map((agent) => ({
|
|
2156
|
+
name: `${agent.name} (${agent.source})`,
|
|
2157
|
+
value: agent.name
|
|
2158
|
+
}))
|
|
2159
|
+
}
|
|
2160
|
+
]);
|
|
2161
|
+
return selected;
|
|
2162
|
+
}
|
|
2163
|
+
async function printDelegateStatus(delegateSecretKey, network) {
|
|
2164
|
+
const signer = await signerFromSecretKeyBase58(delegateSecretKey);
|
|
2165
|
+
const rpc = createSolanaRpc(getRpcUrl());
|
|
2166
|
+
let gasLine = "";
|
|
2167
|
+
try {
|
|
2168
|
+
const { value: balance } = await rpc.getBalance(address(signer.address)).send();
|
|
2169
|
+
gasLine = ` SOL (gas): ${formatSol(Number(balance))} (${balance} lamports)`;
|
|
2170
|
+
} catch {
|
|
2171
|
+
gasLine = " SOL (gas): <balance unavailable>";
|
|
2172
|
+
}
|
|
2173
|
+
console.log(`
|
|
2174
|
+
Delegate key (spl-approve):`);
|
|
2175
|
+
console.log(` Pubkey: ${signer.address}`);
|
|
2176
|
+
console.log(gasLine);
|
|
2177
|
+
console.log(
|
|
2178
|
+
`
|
|
2179
|
+
Fund this address with a small amount of SOL for transaction gas.
|
|
2180
|
+
It is published on this agent's capability cards that declare delegation.
|
|
2181
|
+
`
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
async function cmdDelegateKey(name, options) {
|
|
2185
|
+
const cwd = process.cwd();
|
|
2186
|
+
const agentName = await resolveAgentName(name, cwd, options.yes === true);
|
|
2187
|
+
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
2188
|
+
const loaded = await loadAgent(agentName, cwd, passphrase);
|
|
2189
|
+
loaded.yaml.payments.find((entry) => entry.chain === "solana")?.network ?? "devnet";
|
|
2190
|
+
const existing = loaded.secrets.solana_delegate_secret_key;
|
|
2191
|
+
if (options.show) {
|
|
2192
|
+
if (!existing) {
|
|
2193
|
+
console.error(
|
|
2194
|
+
`Agent "${agentName}" has no delegate key. Run \`elisym delegate-key ${agentName}\` to create one.`
|
|
2195
|
+
);
|
|
2196
|
+
process.exit(1);
|
|
2197
|
+
}
|
|
2198
|
+
await printDelegateStatus(existing);
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (existing && !options.rotate) {
|
|
2202
|
+
console.log(` Agent "${agentName}" already has a delegate key.`);
|
|
2203
|
+
await printDelegateStatus(existing);
|
|
2204
|
+
console.log(" Use --rotate to replace it (this invalidates outstanding owner approvals).");
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const nonInteractive = options.yes === true;
|
|
2208
|
+
if (options.rotate && existing && !nonInteractive) {
|
|
2209
|
+
const { default: inquirer } = await import('inquirer');
|
|
2210
|
+
const { confirmRotate } = await inquirer.prompt([
|
|
2211
|
+
{
|
|
2212
|
+
type: "confirm",
|
|
2213
|
+
name: "confirmRotate",
|
|
2214
|
+
default: false,
|
|
2215
|
+
message: "Rotating the delegate key invalidates every outstanding owner approval (owners must re-approve the new key). Continue?"
|
|
2216
|
+
}
|
|
2217
|
+
]);
|
|
2218
|
+
if (!confirmRotate) {
|
|
2219
|
+
console.log(" Aborted. Delegate key unchanged.");
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
let delegateSecretKey;
|
|
2224
|
+
if (options.import) {
|
|
2225
|
+
if (nonInteractive) {
|
|
2226
|
+
console.error(
|
|
2227
|
+
"Import is interactive (it prompts for the key). Re-run without --yes to import."
|
|
2228
|
+
);
|
|
2229
|
+
process.exit(1);
|
|
2230
|
+
}
|
|
2231
|
+
const { default: inquirer } = await import('inquirer');
|
|
2232
|
+
const { imported } = await inquirer.prompt([
|
|
2233
|
+
{
|
|
2234
|
+
type: "password",
|
|
2235
|
+
mask: "*",
|
|
2236
|
+
name: "imported",
|
|
2237
|
+
message: "Path to a solana-keygen JSON file (or paste a base58 secret key):"
|
|
2238
|
+
}
|
|
2239
|
+
]);
|
|
2240
|
+
try {
|
|
2241
|
+
delegateSecretKey = await parseImportedSecret(imported);
|
|
2242
|
+
} catch (error) {
|
|
2243
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2244
|
+
process.exit(1);
|
|
2245
|
+
}
|
|
2246
|
+
} else {
|
|
2247
|
+
const wallet = await generateSolanaWallet();
|
|
2248
|
+
delegateSecretKey = wallet.secretKeyBase58;
|
|
2249
|
+
}
|
|
2250
|
+
if (loaded.secrets.solana_secret_key) {
|
|
2251
|
+
const paymentSigner = await signerFromSecretKeyBase58(loaded.secrets.solana_secret_key);
|
|
2252
|
+
const delegateSigner = await signerFromSecretKeyBase58(delegateSecretKey);
|
|
2253
|
+
if (paymentSigner.address === delegateSigner.address) {
|
|
2254
|
+
console.error(
|
|
2255
|
+
"The delegate key must differ from the agent payment key (solana_secret_key) for blast-radius isolation."
|
|
2256
|
+
);
|
|
2257
|
+
process.exit(1);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
await writeSecrets(
|
|
2261
|
+
loaded.dir,
|
|
2262
|
+
{ ...loaded.secrets, solana_delegate_secret_key: delegateSecretKey },
|
|
2263
|
+
passphrase
|
|
2264
|
+
);
|
|
2265
|
+
console.log(
|
|
2266
|
+
existing ? "\n Delegate key rotated and saved." : "\n Delegate key generated and saved."
|
|
2267
|
+
);
|
|
2268
|
+
await printDelegateStatus(delegateSecretKey);
|
|
2269
|
+
}
|
|
2270
|
+
var IDENTITY_PLATFORMS = ["github", "x", "website"];
|
|
2271
|
+
var X_PROOF_URL_HOSTS = /* @__PURE__ */ new Set([
|
|
2272
|
+
"x.com",
|
|
2273
|
+
"www.x.com",
|
|
2274
|
+
"mobile.x.com",
|
|
2275
|
+
"twitter.com",
|
|
2276
|
+
"www.twitter.com",
|
|
2277
|
+
"mobile.twitter.com"
|
|
2278
|
+
]);
|
|
2279
|
+
function githubProofText(npub) {
|
|
2280
|
+
return `Verifying that I control the following Nostr public key: ${npub}`;
|
|
2281
|
+
}
|
|
2282
|
+
function xProofText(npub) {
|
|
2283
|
+
return `Verifying my account on nostr My Public Key: "${npub}"`;
|
|
2284
|
+
}
|
|
2285
|
+
function websiteNostrJson(local, pubkeyHex) {
|
|
2286
|
+
return JSON.stringify({ names: { [local]: pubkeyHex } }, null, 2);
|
|
2287
|
+
}
|
|
2288
|
+
function parseIdentityPlatform(raw) {
|
|
2289
|
+
const lowered = raw.trim().toLowerCase();
|
|
2290
|
+
const match = IDENTITY_PLATFORMS.find((platform) => platform === lowered);
|
|
2291
|
+
if (match === void 0) {
|
|
2292
|
+
throw new Error(`Unknown platform "${raw}". Expected github, x, or website.`);
|
|
2293
|
+
}
|
|
2294
|
+
return match;
|
|
2295
|
+
}
|
|
2296
|
+
function parseGithubProofInput(raw) {
|
|
2297
|
+
const trimmed = raw.trim();
|
|
2298
|
+
const bareId = trimmed.toLowerCase();
|
|
2299
|
+
if (GIST_ID_REGEX.test(bareId)) {
|
|
2300
|
+
return { proofId: bareId };
|
|
2301
|
+
}
|
|
2302
|
+
let url;
|
|
2303
|
+
try {
|
|
2304
|
+
url = new URL(trimmed);
|
|
2305
|
+
} catch {
|
|
2306
|
+
return null;
|
|
2307
|
+
}
|
|
2308
|
+
if (url.protocol !== "https:" || url.hostname.toLowerCase() !== "gist.github.com") {
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
2311
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
2312
|
+
const [userSegment, gistSegment] = segments;
|
|
2313
|
+
if (userSegment === void 0 || gistSegment === void 0) {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
const username = userSegment.toLowerCase();
|
|
2317
|
+
const gistId = gistSegment.toLowerCase();
|
|
2318
|
+
if (!GITHUB_USERNAME_REGEX.test(username) || !GIST_ID_REGEX.test(gistId)) {
|
|
2319
|
+
return null;
|
|
2320
|
+
}
|
|
2321
|
+
return { proofId: gistId, username };
|
|
2322
|
+
}
|
|
2323
|
+
function parseXProofInput(raw) {
|
|
2324
|
+
const trimmed = raw.trim();
|
|
2325
|
+
if (TWEET_ID_REGEX.test(trimmed)) {
|
|
2326
|
+
return { proofId: trimmed };
|
|
2327
|
+
}
|
|
2328
|
+
let url;
|
|
2329
|
+
try {
|
|
2330
|
+
url = new URL(trimmed);
|
|
2331
|
+
} catch {
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
if (url.protocol !== "https:" || !X_PROOF_URL_HOSTS.has(url.hostname.toLowerCase())) {
|
|
2335
|
+
return null;
|
|
2336
|
+
}
|
|
2337
|
+
const segments = url.pathname.split("/").filter((segment) => segment.length > 0);
|
|
2338
|
+
const [userSegment, statusLiteral, idSegment] = segments;
|
|
2339
|
+
if (userSegment === void 0 || statusLiteral !== "status" || idSegment === void 0) {
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
const username = userSegment.toLowerCase();
|
|
2343
|
+
if (!X_USERNAME_REGEX.test(username) || !TWEET_ID_REGEX.test(idSegment)) {
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
return { proofId: idSegment, username };
|
|
2347
|
+
}
|
|
2348
|
+
function buildIdentityClaimInputs(identities) {
|
|
2349
|
+
const claims = [];
|
|
2350
|
+
if (identities?.github !== void 0) {
|
|
2351
|
+
claims.push({
|
|
2352
|
+
platform: "github",
|
|
2353
|
+
handle: identities.github.username,
|
|
2354
|
+
proofId: identities.github.gist
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
if (identities?.x !== void 0) {
|
|
2358
|
+
claims.push({ platform: "x", handle: identities.x.username, proofId: identities.x.tweet });
|
|
2359
|
+
}
|
|
2360
|
+
return claims;
|
|
2361
|
+
}
|
|
2362
|
+
function identitiesFromYaml(identities) {
|
|
2363
|
+
const claims = [];
|
|
2364
|
+
if (identities === void 0) {
|
|
2365
|
+
return claims;
|
|
1531
2366
|
}
|
|
1532
|
-
if (identities?.x !== void 0) {
|
|
1533
|
-
claims.push({ platform: "x", handle: identities.x.username, proofId: identities.x.tweet });
|
|
1534
|
-
}
|
|
1535
|
-
return claims;
|
|
1536
|
-
}
|
|
1537
|
-
function identitiesFromYaml(identities) {
|
|
1538
|
-
const claims = [];
|
|
1539
|
-
if (identities === void 0) {
|
|
1540
|
-
return claims;
|
|
1541
|
-
}
|
|
1542
2367
|
if (identities.github !== void 0) {
|
|
1543
2368
|
claims.push({
|
|
1544
2369
|
platform: "github",
|
|
@@ -2262,74 +3087,35 @@ async function probeRelays(relays, logger, timeoutMs = TCP_PROBE_TIMEOUT_MS) {
|
|
|
2262
3087
|
continue;
|
|
2263
3088
|
}
|
|
2264
3089
|
const { host, port } = parsed;
|
|
2265
|
-
let ips = [];
|
|
2266
|
-
try {
|
|
2267
|
-
const resolved = await lookup(host, { all: true });
|
|
2268
|
-
ips = resolved.map((entry) => entry.address);
|
|
2269
|
-
} catch (error) {
|
|
2270
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2271
|
-
logger.debug(
|
|
2272
|
-
{ event: "net_diag_dns_failed", url, host, error: message },
|
|
2273
|
-
"DNS lookup failed"
|
|
2274
|
-
);
|
|
2275
|
-
results.push({ url, host, port, ips: [], error: `dns: ${message}` });
|
|
2276
|
-
continue;
|
|
2277
|
-
}
|
|
2278
|
-
const probe = await tcpProbe(host, port, timeoutMs);
|
|
2279
|
-
if ("openMs" in probe) {
|
|
2280
|
-
logger.debug(
|
|
2281
|
-
{ event: "net_diag_tcp_open", url, host, port, ips, tcpOpenMs: probe.openMs },
|
|
2282
|
-
"TCP open"
|
|
2283
|
-
);
|
|
2284
|
-
results.push({ url, host, port, ips, tcpOpenMs: probe.openMs });
|
|
2285
|
-
} else {
|
|
2286
|
-
logger.debug(
|
|
2287
|
-
{ event: "net_diag_tcp_failed", url, host, port, ips, error: probe.error },
|
|
2288
|
-
"TCP probe failed"
|
|
2289
|
-
);
|
|
2290
|
-
results.push({ url, host, port, ips, error: `tcp: ${probe.error}` });
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
return results;
|
|
2294
|
-
}
|
|
2295
|
-
var MAX_CONCURRENT_JOBS = 10;
|
|
2296
|
-
var RECOVERY_MAX_RETRIES = 5;
|
|
2297
|
-
var RECOVERY_INTERVAL_SECS = 60;
|
|
2298
|
-
var WATCHDOG_PROBE_INTERVAL_MS = 5 * 60 * 1e3;
|
|
2299
|
-
var WATCHDOG_PROBE_TIMEOUT_MS = 1e4;
|
|
2300
|
-
var WATCHDOG_SELF_PING_INTERVAL_MS = 10 * 60 * 1e3;
|
|
2301
|
-
var WATCHDOG_SELF_PING_TIMEOUT_MS = 15e3;
|
|
2302
|
-
var WATCHDOG_SLEEP_DETECT_MULTIPLIER = 2;
|
|
2303
|
-
function getRpcUrl(_network) {
|
|
2304
|
-
const envUrl = process.env.SOLANA_RPC_URL;
|
|
2305
|
-
if (envUrl) {
|
|
2306
|
-
return envUrl;
|
|
2307
|
-
}
|
|
2308
|
-
return "https://api.devnet.solana.com";
|
|
2309
|
-
}
|
|
2310
|
-
async function fetchUsdcBalance(rpc, owner) {
|
|
2311
|
-
const mint = USDC_SOLANA_DEVNET.mint;
|
|
2312
|
-
if (!mint) {
|
|
2313
|
-
return 0n;
|
|
2314
|
-
}
|
|
2315
|
-
try {
|
|
2316
|
-
const response = await rpc.getTokenAccountsByOwner(
|
|
2317
|
-
owner,
|
|
2318
|
-
{ mint: address(mint) },
|
|
2319
|
-
{ encoding: "jsonParsed", commitment: "confirmed" }
|
|
2320
|
-
).send();
|
|
2321
|
-
let total = 0n;
|
|
2322
|
-
for (const entry of response.value) {
|
|
2323
|
-
const parsed = entry.account.data;
|
|
2324
|
-
const raw = parsed?.parsed?.info?.tokenAmount?.amount;
|
|
2325
|
-
if (typeof raw === "string") {
|
|
2326
|
-
total += BigInt(raw);
|
|
2327
|
-
}
|
|
3090
|
+
let ips = [];
|
|
3091
|
+
try {
|
|
3092
|
+
const resolved = await lookup(host, { all: true });
|
|
3093
|
+
ips = resolved.map((entry) => entry.address);
|
|
3094
|
+
} catch (error) {
|
|
3095
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3096
|
+
logger.debug(
|
|
3097
|
+
{ event: "net_diag_dns_failed", url, host, error: message },
|
|
3098
|
+
"DNS lookup failed"
|
|
3099
|
+
);
|
|
3100
|
+
results.push({ url, host, port, ips: [], error: `dns: ${message}` });
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
const probe = await tcpProbe(host, port, timeoutMs);
|
|
3104
|
+
if ("openMs" in probe) {
|
|
3105
|
+
logger.debug(
|
|
3106
|
+
{ event: "net_diag_tcp_open", url, host, port, ips, tcpOpenMs: probe.openMs },
|
|
3107
|
+
"TCP open"
|
|
3108
|
+
);
|
|
3109
|
+
results.push({ url, host, port, ips, tcpOpenMs: probe.openMs });
|
|
3110
|
+
} else {
|
|
3111
|
+
logger.debug(
|
|
3112
|
+
{ event: "net_diag_tcp_failed", url, host, port, ips, error: probe.error },
|
|
3113
|
+
"TCP probe failed"
|
|
3114
|
+
);
|
|
3115
|
+
results.push({ url, host, port, ips, error: `tcp: ${probe.error}` });
|
|
2328
3116
|
}
|
|
2329
|
-
return total;
|
|
2330
|
-
} catch {
|
|
2331
|
-
return 0n;
|
|
2332
3117
|
}
|
|
3118
|
+
return results;
|
|
2333
3119
|
}
|
|
2334
3120
|
var LEDGER_DIR_MODE = 448;
|
|
2335
3121
|
var LEDGER_FILE_MODE = 384;
|
|
@@ -2398,6 +3184,40 @@ var JobLedger = class {
|
|
|
2398
3184
|
this.flush();
|
|
2399
3185
|
}
|
|
2400
3186
|
}
|
|
3187
|
+
/** Flush the delegated discriminator (called inside the atomic nonce mark). */
|
|
3188
|
+
markDelegated(jobId) {
|
|
3189
|
+
const entry = this.entries.get(jobId);
|
|
3190
|
+
if (entry) {
|
|
3191
|
+
entry.delegated = true;
|
|
3192
|
+
this.flush();
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Persist a delegated job's result text BEFORE the pull, while status stays
|
|
3197
|
+
* `paid`. MUST be flushed strictly before `recordPullSignature` so a
|
|
3198
|
+
* persisted pull signature always implies the result is recoverable.
|
|
3199
|
+
*/
|
|
3200
|
+
recordDeliveredContent(jobId, deliveredContent) {
|
|
3201
|
+
const entry = this.entries.get(jobId);
|
|
3202
|
+
if (entry) {
|
|
3203
|
+
entry.delivered_content = deliveredContent;
|
|
3204
|
+
this.flush();
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Phase-A persist of the two-phase pull: the signature (durable idempotency
|
|
3209
|
+
* key), its blockhash terminal bound, and the amount the pull moves - one
|
|
3210
|
+
* flush, BEFORE any bytes hit the network.
|
|
3211
|
+
*/
|
|
3212
|
+
recordPullSignature(jobId, pullSignature, pullLastValidBlockHeight, netAmount) {
|
|
3213
|
+
const entry = this.entries.get(jobId);
|
|
3214
|
+
if (entry) {
|
|
3215
|
+
entry.pull_signature = pullSignature;
|
|
3216
|
+
entry.pull_last_valid_block_height = pullLastValidBlockHeight;
|
|
3217
|
+
entry.net_amount = netAmount;
|
|
3218
|
+
this.flush();
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
2401
3221
|
/** Attempt a state transition. Returns the entry if valid, undefined otherwise. */
|
|
2402
3222
|
transition(jobId, to) {
|
|
2403
3223
|
const entry = this.entries.get(jobId);
|
|
@@ -2438,6 +3258,7 @@ var JobLedger = class {
|
|
|
2438
3258
|
const entry = this.transition(jobId, "delivered");
|
|
2439
3259
|
if (entry) {
|
|
2440
3260
|
entry.result = void 0;
|
|
3261
|
+
entry.delivered_content = void 0;
|
|
2441
3262
|
this.flush();
|
|
2442
3263
|
}
|
|
2443
3264
|
}
|
|
@@ -2445,6 +3266,7 @@ var JobLedger = class {
|
|
|
2445
3266
|
const entry = this.transition(jobId, "failed");
|
|
2446
3267
|
if (entry) {
|
|
2447
3268
|
entry.result = void 0;
|
|
3269
|
+
entry.delivered_content = void 0;
|
|
2448
3270
|
try {
|
|
2449
3271
|
this.flush();
|
|
2450
3272
|
} catch {
|
|
@@ -2464,6 +3286,10 @@ var JobLedger = class {
|
|
|
2464
3286
|
pendingJobs() {
|
|
2465
3287
|
return [...this.entries.values()].filter((e) => e.status === "paid" || e.status === "executed");
|
|
2466
3288
|
}
|
|
3289
|
+
/** Every entry, any status. Used to reconcile the used-nonce set on restart. */
|
|
3290
|
+
allEntries() {
|
|
3291
|
+
return [...this.entries.values()];
|
|
3292
|
+
}
|
|
2467
3293
|
/** Remove old delivered/failed entries (default: 7 days). */
|
|
2468
3294
|
gc(maxAgeSecs = 7 * 24 * 60 * 60) {
|
|
2469
3295
|
this.pruneOldEntries(maxAgeSecs * 1e3);
|
|
@@ -2491,6 +3317,92 @@ var JobLedger = class {
|
|
|
2491
3317
|
return deleted;
|
|
2492
3318
|
}
|
|
2493
3319
|
};
|
|
3320
|
+
var NONCE_STORE_MAX_ENTRIES = 1e4;
|
|
3321
|
+
var UsedNonceStore = class {
|
|
3322
|
+
/** key -> retain-until (unix seconds). Map order doubles as mark order for evict-oldest. */
|
|
3323
|
+
entries = /* @__PURE__ */ new Map();
|
|
3324
|
+
path;
|
|
3325
|
+
maxEntries;
|
|
3326
|
+
constructor(noncePath, maxEntries = NONCE_STORE_MAX_ENTRIES) {
|
|
3327
|
+
this.path = noncePath;
|
|
3328
|
+
this.maxEntries = maxEntries;
|
|
3329
|
+
this.load();
|
|
3330
|
+
}
|
|
3331
|
+
load() {
|
|
3332
|
+
try {
|
|
3333
|
+
const raw = readFileSync(this.path, "utf-8");
|
|
3334
|
+
const data = JSON.parse(raw);
|
|
3335
|
+
for (const [key, retainUntil] of Object.entries(data)) {
|
|
3336
|
+
if (typeof retainUntil === "number" && Number.isFinite(retainUntil)) {
|
|
3337
|
+
this.entries.set(key, retainUntil);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
} catch (e) {
|
|
3341
|
+
if (e?.code !== "ENOENT") {
|
|
3342
|
+
console.warn(` ! Nonce store load warning: ${e?.message ?? "unknown error"}`);
|
|
3343
|
+
try {
|
|
3344
|
+
const backupPath = this.path + ".corrupt." + Date.now();
|
|
3345
|
+
renameSync(this.path, backupPath);
|
|
3346
|
+
chmodSync(backupPath, LEDGER_FILE_MODE);
|
|
3347
|
+
} catch {
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
flush() {
|
|
3353
|
+
const dir = dirname(this.path);
|
|
3354
|
+
mkdirSync(dir, { recursive: true, mode: LEDGER_DIR_MODE });
|
|
3355
|
+
const obj = Object.fromEntries(this.entries);
|
|
3356
|
+
const tmp = this.path + ".tmp";
|
|
3357
|
+
writeFileSync(tmp, JSON.stringify(obj), { mode: LEDGER_FILE_MODE });
|
|
3358
|
+
renameSync(tmp, this.path);
|
|
3359
|
+
chmodSync(this.path, LEDGER_FILE_MODE);
|
|
3360
|
+
}
|
|
3361
|
+
has(key) {
|
|
3362
|
+
return this.entries.has(key);
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* Burn a nonce. Flushes by default; pass `persist: false` during bulk
|
|
3366
|
+
* restart reconciliation and call {@link flush} once at the end. Evicts the
|
|
3367
|
+
* oldest entry on overflow rather than rejecting.
|
|
3368
|
+
*/
|
|
3369
|
+
markUsed(key, retainUntilSecs, opts = {}) {
|
|
3370
|
+
while (this.entries.size >= this.maxEntries) {
|
|
3371
|
+
const oldest = this.entries.keys().next();
|
|
3372
|
+
if (oldest.done) {
|
|
3373
|
+
break;
|
|
3374
|
+
}
|
|
3375
|
+
this.entries.delete(oldest.value);
|
|
3376
|
+
}
|
|
3377
|
+
this.entries.set(key, retainUntilSecs);
|
|
3378
|
+
if (opts.persist !== false) {
|
|
3379
|
+
try {
|
|
3380
|
+
this.flush();
|
|
3381
|
+
} catch {
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
/** Drop entries past their retain-until. Returns the number pruned. */
|
|
3386
|
+
prune(nowSecs = Math.floor(Date.now() / 1e3)) {
|
|
3387
|
+
let pruned = 0;
|
|
3388
|
+
for (const [key, retainUntil] of this.entries) {
|
|
3389
|
+
if (retainUntil <= nowSecs) {
|
|
3390
|
+
this.entries.delete(key);
|
|
3391
|
+
pruned += 1;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
if (pruned > 0) {
|
|
3395
|
+
try {
|
|
3396
|
+
this.flush();
|
|
3397
|
+
} catch {
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
return pruned;
|
|
3401
|
+
}
|
|
3402
|
+
size() {
|
|
3403
|
+
return this.entries.size;
|
|
3404
|
+
}
|
|
3405
|
+
};
|
|
2494
3406
|
|
|
2495
3407
|
// src/commands/start.ts
|
|
2496
3408
|
init_llm();
|
|
@@ -3532,6 +4444,8 @@ function resolveHealthPair(skill) {
|
|
|
3532
4444
|
}
|
|
3533
4445
|
return null;
|
|
3534
4446
|
}
|
|
4447
|
+
var DELEGATED_REJECTED_PREFIX = "Delegated payment rejected";
|
|
4448
|
+
var RECONCILE_RECHECK_ATTEMPTS = 6;
|
|
3535
4449
|
var RATE_LIMIT_WINDOW_MS = 10 * 60 * 1e3;
|
|
3536
4450
|
var FREE_MAX_JOBS_PER_CUSTOMER = 20;
|
|
3537
4451
|
var FREE_GLOBAL_MAX_JOBS_PER_WINDOW = 200;
|
|
@@ -3540,7 +4454,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
|
3540
4454
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
3541
4455
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
3542
4456
|
var AgentRuntime = class {
|
|
3543
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport, identity2, blossomTransport, sessionStore) {
|
|
4457
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport, identity2, blossomTransport, sessionStore, nonceStore) {
|
|
3544
4458
|
this.transport = transport;
|
|
3545
4459
|
this.skills = skills;
|
|
3546
4460
|
this.skillCtx = skillCtx;
|
|
@@ -3552,6 +4466,7 @@ var AgentRuntime = class {
|
|
|
3552
4466
|
this.identity = identity2;
|
|
3553
4467
|
this.blossomTransport = blossomTransport;
|
|
3554
4468
|
this.sessionStore = sessionStore;
|
|
4469
|
+
this.nonceStore = nonceStore;
|
|
3555
4470
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
3556
4471
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
3557
4472
|
}
|
|
@@ -3599,6 +4514,13 @@ var AgentRuntime = class {
|
|
|
3599
4514
|
* the operator's API key.
|
|
3600
4515
|
*/
|
|
3601
4516
|
freeLlmLimiters = createFreeLlmLimiterSet();
|
|
4517
|
+
/**
|
|
4518
|
+
* Per-owner in-flight delegated reservations (subunits). A concurrency gate,
|
|
4519
|
+
* NOT a budget: released on success AND failure (unlike the MCP session-spend
|
|
4520
|
+
* reservation, which sticks on success). Process-local by design - the
|
|
4521
|
+
* on-chain `delegated_amount` is the real ceiling.
|
|
4522
|
+
*/
|
|
4523
|
+
delegationInFlight = /* @__PURE__ */ new Map();
|
|
3602
4524
|
/**
|
|
3603
4525
|
* Whether a job takes the session path: session ref present (transport only
|
|
3604
4526
|
* populates it for NIP-44-encrypted requests) AND the matched skill opted in
|
|
@@ -3840,6 +4762,7 @@ var AgentRuntime = class {
|
|
|
3840
4762
|
const log = this.callbacks.onLog ?? console.log;
|
|
3841
4763
|
this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
|
|
3842
4764
|
this.sessionStore?.init();
|
|
4765
|
+
this.reconcileUsedNonces();
|
|
3843
4766
|
await this.recoverPendingJobs();
|
|
3844
4767
|
this.recoveryInterval = setInterval(
|
|
3845
4768
|
() => this.recoverPendingJobs().catch((e) => log(`Recovery error: ${e}`)),
|
|
@@ -3856,6 +4779,11 @@ var AgentRuntime = class {
|
|
|
3856
4779
|
} catch (e) {
|
|
3857
4780
|
log(`Session GC error: ${e.message}`);
|
|
3858
4781
|
}
|
|
4782
|
+
try {
|
|
4783
|
+
this.nonceStore?.prune();
|
|
4784
|
+
} catch (e) {
|
|
4785
|
+
log(`Nonce GC error: ${e.message}`);
|
|
4786
|
+
}
|
|
3859
4787
|
this.cleanupRateLimits();
|
|
3860
4788
|
if (!this.transport.isHealthy()) {
|
|
3861
4789
|
log("Warning: no events received in 30+ minutes. Check relay connectivity.");
|
|
@@ -4112,120 +5040,476 @@ var AgentRuntime = class {
|
|
|
4112
5040
|
raw_event_json: JSON.stringify(job.rawEvent),
|
|
4113
5041
|
created_at: Math.floor(Date.now() / 1e3)
|
|
4114
5042
|
});
|
|
4115
|
-
|
|
5043
|
+
const claimsDelegated = job.delegated !== void 0 || job.delegatedRejected !== void 0;
|
|
5044
|
+
let delegatedPull;
|
|
5045
|
+
if (claimsDelegated) {
|
|
5046
|
+
delegatedPull = await this.delegatedPreCheck(job, matched, log);
|
|
5047
|
+
if (delegatedPull === void 0) {
|
|
5048
|
+
return;
|
|
5049
|
+
}
|
|
5050
|
+
netAmount = Number(delegatedPull.priceSubunits);
|
|
5051
|
+
} else if (jobPrice > 0) {
|
|
4116
5052
|
const result = await this.collectPayment(job, jobPrice, jobAsset, signal);
|
|
4117
5053
|
netAmount = result.netAmount;
|
|
4118
5054
|
paymentRequest = result.paymentRequest;
|
|
4119
5055
|
this.ledger.updatePayment(job.jobId, netAmount, paymentRequest);
|
|
4120
5056
|
log(
|
|
4121
|
-
`[${job.jobId.slice(0, 8)}] Payment confirmed: ${formatAssetAmount(
|
|
4122
|
-
jobAsset,
|
|
4123
|
-
BigInt(netAmount)
|
|
4124
|
-
)}`
|
|
5057
|
+
`[${job.jobId.slice(0, 8)}] Payment confirmed: ${formatAssetAmount(
|
|
5058
|
+
jobAsset,
|
|
5059
|
+
BigInt(netAmount)
|
|
5060
|
+
)}`
|
|
5061
|
+
);
|
|
5062
|
+
this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
|
|
5063
|
+
}
|
|
5064
|
+
try {
|
|
5065
|
+
const inputFile = await this.resolveInputFile(job.attachment, job.customerId, signal);
|
|
5066
|
+
await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
|
|
5067
|
+
});
|
|
5068
|
+
const skill = this.skills.route(job.tags);
|
|
5069
|
+
if (!skill) {
|
|
5070
|
+
throw new Error("No skill matched for tags: " + job.tags.join(", "));
|
|
5071
|
+
}
|
|
5072
|
+
log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
|
|
5073
|
+
const budgetMs = this.resolveExecutionBudgetMs(skill);
|
|
5074
|
+
const jobSession = this.resolveJobSession(job, skill);
|
|
5075
|
+
const runExecution = async (history) => {
|
|
5076
|
+
let budgetExceeded = false;
|
|
5077
|
+
const execAbort = new AbortController();
|
|
5078
|
+
const onOuterAbort = () => execAbort.abort();
|
|
5079
|
+
if (signal) {
|
|
5080
|
+
if (signal.aborted) {
|
|
5081
|
+
execAbort.abort();
|
|
5082
|
+
} else {
|
|
5083
|
+
signal.addEventListener("abort", onOuterAbort);
|
|
5084
|
+
}
|
|
5085
|
+
}
|
|
5086
|
+
let budgetTimer;
|
|
5087
|
+
try {
|
|
5088
|
+
const execPromise = skill.execute(
|
|
5089
|
+
{
|
|
5090
|
+
data: inputFile?.inlineText ?? job.input,
|
|
5091
|
+
inputType: job.inputType,
|
|
5092
|
+
tags: job.tags,
|
|
5093
|
+
jobId: job.jobId,
|
|
5094
|
+
filePath: inputFile?.filePath,
|
|
5095
|
+
history,
|
|
5096
|
+
...jobSession !== null ? { sessionId: jobSession.sessionId } : {}
|
|
5097
|
+
},
|
|
5098
|
+
{ ...this.skillCtx, signal: execAbort.signal }
|
|
5099
|
+
);
|
|
5100
|
+
execPromise.catch(() => {
|
|
5101
|
+
});
|
|
5102
|
+
if (budgetMs > 0) {
|
|
5103
|
+
return await Promise.race([
|
|
5104
|
+
execPromise,
|
|
5105
|
+
new Promise((_resolve, reject) => {
|
|
5106
|
+
budgetTimer = setTimeout(() => {
|
|
5107
|
+
budgetExceeded = true;
|
|
5108
|
+
execAbort.abort();
|
|
5109
|
+
reject(new ExecutionBudgetExceededError(budgetMs));
|
|
5110
|
+
}, budgetMs);
|
|
5111
|
+
})
|
|
5112
|
+
]);
|
|
5113
|
+
}
|
|
5114
|
+
return await execPromise;
|
|
5115
|
+
} catch (err) {
|
|
5116
|
+
if (budgetExceeded) {
|
|
5117
|
+
log(`[${job.jobId.slice(0, 8)}] Execution exceeded budget (${budgetMs / 1e3}s)`);
|
|
5118
|
+
throw new ExecutionBudgetExceededError(budgetMs);
|
|
5119
|
+
}
|
|
5120
|
+
const flippedToUnhealthy = this.markHealthFromExecuteError(skill, err, log, job.jobId);
|
|
5121
|
+
if (flippedToUnhealthy) {
|
|
5122
|
+
throw new AgentUnavailableError();
|
|
5123
|
+
}
|
|
5124
|
+
throw err;
|
|
5125
|
+
} finally {
|
|
5126
|
+
if (budgetTimer) {
|
|
5127
|
+
clearTimeout(budgetTimer);
|
|
5128
|
+
}
|
|
5129
|
+
if (signal) {
|
|
5130
|
+
signal.removeEventListener("abort", onOuterAbort);
|
|
5131
|
+
}
|
|
5132
|
+
if (inputFile) {
|
|
5133
|
+
await inputFile.cleanup().catch(() => {
|
|
5134
|
+
});
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
};
|
|
5138
|
+
const output = jobSession === null ? await runExecution() : await this.runSessionExchange({
|
|
5139
|
+
session: jobSession,
|
|
5140
|
+
jobId: job.jobId,
|
|
5141
|
+
userRecord: buildUserRecord(
|
|
5142
|
+
inputFile?.inlineText ?? job.input,
|
|
5143
|
+
inputFile?.filePath !== void 0 ? job.attachment?.name ?? "attachment" : void 0
|
|
5144
|
+
),
|
|
5145
|
+
signal,
|
|
5146
|
+
log,
|
|
5147
|
+
execute: runExecution
|
|
5148
|
+
});
|
|
5149
|
+
const { attachments, deliveredContent } = await this.buildResultAttachment(
|
|
5150
|
+
job.jobId,
|
|
5151
|
+
output,
|
|
5152
|
+
job.customerId,
|
|
5153
|
+
readAcceptedTransports(job.rawEvent.tags)
|
|
5154
|
+
);
|
|
5155
|
+
if (delegatedPull !== void 0) {
|
|
5156
|
+
if (deliveredContent === "" && attachments.length === 0) {
|
|
5157
|
+
this.ledger.markFailed(job.jobId);
|
|
5158
|
+
await this.transport.sendFeedback(job, {
|
|
5159
|
+
type: "error",
|
|
5160
|
+
message: `${DELEGATED_REJECTED_PREFIX}: the skill produced empty output - you were not charged. Please re-submit.`
|
|
5161
|
+
}).catch(() => {
|
|
5162
|
+
});
|
|
5163
|
+
return;
|
|
5164
|
+
}
|
|
5165
|
+
this.ledger.recordDeliveredContent(job.jobId, deliveredContent);
|
|
5166
|
+
const outcome = await this.executeDelegatedPull(job, delegatedPull, log);
|
|
5167
|
+
if (outcome === "dead") {
|
|
5168
|
+
this.ledger.markFailed(job.jobId);
|
|
5169
|
+
await this.transport.sendFeedback(job, {
|
|
5170
|
+
type: "error",
|
|
5171
|
+
message: `${DELEGATED_REJECTED_PREFIX}: the delegated transfer did not land - you were not charged. Please re-submit.`
|
|
5172
|
+
}).catch(() => {
|
|
5173
|
+
});
|
|
5174
|
+
return;
|
|
5175
|
+
}
|
|
5176
|
+
if (outcome === "unresolved") {
|
|
5177
|
+
log(
|
|
5178
|
+
`[${job.jobId.slice(0, 8)}] Delegated pull unresolved before terminal; recovery will reconcile.`
|
|
5179
|
+
);
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5183
|
+
this.ledger.markExecuted(job.jobId, deliveredContent);
|
|
5184
|
+
log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
|
|
5185
|
+
const eventId = await this.transport.deliverResult(
|
|
5186
|
+
job,
|
|
5187
|
+
deliveredContent,
|
|
5188
|
+
netAmount,
|
|
5189
|
+
attachments.length > 0 ? attachments : void 0,
|
|
5190
|
+
delegatedPull?.pullSignature
|
|
5191
|
+
);
|
|
5192
|
+
this.ledger.markDelivered(job.jobId);
|
|
5193
|
+
log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
|
|
5194
|
+
this.callbacks.onJobCompleted?.(job.jobId, output.data);
|
|
5195
|
+
} finally {
|
|
5196
|
+
if (delegatedPull !== void 0) {
|
|
5197
|
+
this.releaseDelegationReservation(delegatedPull.request.owner, delegatedPull.priceSubunits);
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
reserveDelegation(owner, amount) {
|
|
5202
|
+
this.delegationInFlight.set(owner, (this.delegationInFlight.get(owner) ?? 0n) + amount);
|
|
5203
|
+
}
|
|
5204
|
+
/** Saturates at zero; drops the key when the owner has nothing in flight. */
|
|
5205
|
+
releaseDelegationReservation(owner, amount) {
|
|
5206
|
+
const current = this.delegationInFlight.get(owner) ?? 0n;
|
|
5207
|
+
const next = current > amount ? current - amount : 0n;
|
|
5208
|
+
if (next === 0n) {
|
|
5209
|
+
this.delegationInFlight.delete(owner);
|
|
5210
|
+
} else {
|
|
5211
|
+
this.delegationInFlight.set(owner, next);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
/**
|
|
5215
|
+
* Delegated pre-check: fail-closed gates from format to funds, then the
|
|
5216
|
+
* atomic nonce burn, then the on-chain delegation check + per-owner
|
|
5217
|
+
* reservation. Returns the pull context, or `undefined` after rejecting
|
|
5218
|
+
* (entry marked failed + error feedback sent). Runs AFTER `recordPaid`, so
|
|
5219
|
+
* every reject must `markFailed` to avoid a dangling `paid` entry.
|
|
5220
|
+
*/
|
|
5221
|
+
async delegatedPreCheck(job, skill, log) {
|
|
5222
|
+
const reject = async (operatorDetail, customerReason) => {
|
|
5223
|
+
log(`[${job.jobId.slice(0, 8)}] Delegated payment rejected: ${operatorDetail}`);
|
|
5224
|
+
this.ledger.markFailed(job.jobId);
|
|
5225
|
+
await this.transport.sendFeedback(job, {
|
|
5226
|
+
type: "error",
|
|
5227
|
+
message: `${DELEGATED_REJECTED_PREFIX}: ${customerReason}`
|
|
5228
|
+
}).catch(() => {
|
|
5229
|
+
});
|
|
5230
|
+
return void 0;
|
|
5231
|
+
};
|
|
5232
|
+
if (job.delegatedRejected !== void 0 || job.delegated === void 0) {
|
|
5233
|
+
return reject(
|
|
5234
|
+
`malformed delegation tags (${job.delegatedRejected ?? "missing struct"})`,
|
|
5235
|
+
"malformed delegation tags."
|
|
5236
|
+
);
|
|
5237
|
+
}
|
|
5238
|
+
const request = job.delegated;
|
|
5239
|
+
const delegateSigner = this.config.delegateSigner;
|
|
5240
|
+
if (delegateSigner === void 0 || this.nonceStore === void 0) {
|
|
5241
|
+
return reject(
|
|
5242
|
+
"agent has no delegate key configured",
|
|
5243
|
+
"this agent does not accept delegated payment."
|
|
5244
|
+
);
|
|
5245
|
+
}
|
|
5246
|
+
if (this.config.network !== "devnet") {
|
|
5247
|
+
return reject(
|
|
5248
|
+
`delegation is devnet-only (network: ${this.config.network})`,
|
|
5249
|
+
"this agent does not accept delegated payment."
|
|
5250
|
+
);
|
|
5251
|
+
}
|
|
5252
|
+
const network = this.config.network;
|
|
5253
|
+
if (!skill || skill.delegation === void 0 || skill.priceSubunits <= 0) {
|
|
5254
|
+
return reject(
|
|
5255
|
+
"skill does not accept delegated payment",
|
|
5256
|
+
"this skill does not accept delegated payment."
|
|
5257
|
+
);
|
|
5258
|
+
}
|
|
5259
|
+
const delegationAsset = resolveDelegationAsset(network);
|
|
5260
|
+
if (skill.asset.mint !== delegationAsset.mint) {
|
|
5261
|
+
return reject(
|
|
5262
|
+
`skill asset ${skill.asset.symbol} is not the delegation asset`,
|
|
5263
|
+
"this skill does not accept delegated payment."
|
|
5264
|
+
);
|
|
5265
|
+
}
|
|
5266
|
+
if (!this.config.solanaAddress) {
|
|
5267
|
+
return reject(
|
|
5268
|
+
"no solana payment address configured",
|
|
5269
|
+
"this agent does not accept delegated payment."
|
|
5270
|
+
);
|
|
5271
|
+
}
|
|
5272
|
+
const nowSecs = Math.floor(Date.now() / 1e3);
|
|
5273
|
+
if (request.expiryUnix <= nowSecs) {
|
|
5274
|
+
return reject("proof expired", "the payment proof has expired - re-submit the job.");
|
|
5275
|
+
}
|
|
5276
|
+
if (request.expiryUnix > nowSecs + MAX_PROOF_TTL_SECS + PROOF_CLOCK_SKEW_SECS) {
|
|
5277
|
+
return reject(
|
|
5278
|
+
"proof expiry beyond the allowed horizon",
|
|
5279
|
+
"the payment proof expiry is too far in the future."
|
|
5280
|
+
);
|
|
5281
|
+
}
|
|
5282
|
+
const proofValid = await verifyDelegationAuthProof({
|
|
5283
|
+
agentDelegate: delegateSigner.address,
|
|
5284
|
+
nostrAuthor: job.customerId,
|
|
5285
|
+
owner: request.owner,
|
|
5286
|
+
expiryUnix: request.expiryUnix,
|
|
5287
|
+
nonce: request.nonce,
|
|
5288
|
+
proof: request.proof
|
|
5289
|
+
});
|
|
5290
|
+
if (!proofValid) {
|
|
5291
|
+
return reject("proof verification failed", "invalid payment proof.");
|
|
5292
|
+
}
|
|
5293
|
+
const nonceKey = `${request.owner}:${request.nonce}`;
|
|
5294
|
+
if (this.nonceStore.has(nonceKey)) {
|
|
5295
|
+
return reject("nonce already used", "this payment proof was already used.");
|
|
5296
|
+
}
|
|
5297
|
+
this.nonceStore.markUsed(nonceKey, request.expiryUnix + PROOF_CLOCK_SKEW_SECS);
|
|
5298
|
+
this.ledger.markDelegated(job.jobId);
|
|
5299
|
+
let ownerAta;
|
|
5300
|
+
try {
|
|
5301
|
+
ownerAta = await deriveOwnerDelegationAta(request.owner, network);
|
|
5302
|
+
} catch (error) {
|
|
5303
|
+
return reject(
|
|
5304
|
+
`owner ATA derivation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
5305
|
+
"invalid delegation owner address."
|
|
5306
|
+
);
|
|
5307
|
+
}
|
|
5308
|
+
const rpc = createSolanaRpc(getRpcUrl(this.config.network));
|
|
5309
|
+
let status;
|
|
5310
|
+
try {
|
|
5311
|
+
status = await getDelegation(rpc, ownerAta);
|
|
5312
|
+
} catch (error) {
|
|
5313
|
+
return reject(
|
|
5314
|
+
`getDelegation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
5315
|
+
"could not verify the delegation on-chain - try again later."
|
|
5316
|
+
);
|
|
5317
|
+
}
|
|
5318
|
+
if (status === null || status.delegate !== delegateSigner.address) {
|
|
5319
|
+
return reject(
|
|
5320
|
+
`no active delegation for this delegate (current: ${status?.delegate ?? "none"})`,
|
|
5321
|
+
"no active delegation for this agent - approve it first."
|
|
5322
|
+
);
|
|
5323
|
+
}
|
|
5324
|
+
const priceSubunits = BigInt(skill.priceSubunits);
|
|
5325
|
+
const otherInFlight = this.delegationInFlight.get(request.owner) ?? 0n;
|
|
5326
|
+
if (status.remainingCap - otherInFlight < priceSubunits) {
|
|
5327
|
+
return reject(
|
|
5328
|
+
`remaining cap ${status.remainingCap} minus in-flight ${otherInFlight} below price ${priceSubunits}`,
|
|
5329
|
+
"the remaining delegated allowance is below this skill price."
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
5332
|
+
if (status.balance < priceSubunits) {
|
|
5333
|
+
return reject(
|
|
5334
|
+
`owner balance ${status.balance} below price ${priceSubunits}`,
|
|
5335
|
+
"the delegation account balance is below this skill price."
|
|
5336
|
+
);
|
|
5337
|
+
}
|
|
5338
|
+
this.reserveDelegation(request.owner, priceSubunits);
|
|
5339
|
+
log(
|
|
5340
|
+
`[${job.jobId.slice(0, 8)}] Delegated pre-check ok: ${formatAssetAmount(
|
|
5341
|
+
delegationAsset,
|
|
5342
|
+
priceSubunits
|
|
5343
|
+
)} from ${request.owner.slice(0, 8)}... (pull after work)`
|
|
5344
|
+
);
|
|
5345
|
+
return { request, delegateSigner, ownerAta, priceSubunits, rpc, network };
|
|
5346
|
+
}
|
|
5347
|
+
/**
|
|
5348
|
+
* The two-phase pull - the ONLY money move of a delegated job. Phase A signs
|
|
5349
|
+
* and PERSISTS `{signature, lastValidBlockHeight}` (+ the amount) before any
|
|
5350
|
+
* bytes hit the network; phase B sends and polls to a provable terminal.
|
|
5351
|
+
* Never throws past phase A: the outcome routes the caller.
|
|
5352
|
+
*/
|
|
5353
|
+
async executeDelegatedPull(job, ctx, log) {
|
|
5354
|
+
if (!this.config.solanaAddress) {
|
|
5355
|
+
throw new Error("Solana address not configured");
|
|
5356
|
+
}
|
|
5357
|
+
const destinationAta = await deriveOwnerDelegationAta(this.config.solanaAddress, ctx.network);
|
|
5358
|
+
const instructions = await buildDelegatedTransfer({
|
|
5359
|
+
delegate: ctx.delegateSigner,
|
|
5360
|
+
source: ctx.ownerAta,
|
|
5361
|
+
destination: destinationAta,
|
|
5362
|
+
amount: ctx.priceSubunits,
|
|
5363
|
+
network: ctx.network,
|
|
5364
|
+
// Idempotent create (delegate-paid rent) so a fresh provider wallet's
|
|
5365
|
+
// first delegated job does not dead-letter on a missing ATA.
|
|
5366
|
+
ensureDestination: { owner: this.config.solanaAddress }
|
|
5367
|
+
});
|
|
5368
|
+
const pull = await buildSignedPull(ctx.rpc, ctx.delegateSigner, instructions);
|
|
5369
|
+
this.ledger.recordPullSignature(
|
|
5370
|
+
job.jobId,
|
|
5371
|
+
pull.signature,
|
|
5372
|
+
Number(pull.lastValidBlockHeight),
|
|
5373
|
+
Number(ctx.priceSubunits)
|
|
5374
|
+
);
|
|
5375
|
+
ctx.pullSignature = pull.signature;
|
|
5376
|
+
const outcome = await sendConfirmToTerminal(ctx.rpc, pull, {});
|
|
5377
|
+
log(`[${job.jobId.slice(0, 8)}] Delegated pull ${outcome}: ${pull.signature.slice(0, 16)}...`);
|
|
5378
|
+
return outcome;
|
|
5379
|
+
}
|
|
5380
|
+
/**
|
|
5381
|
+
* Restart reconciliation of the durable used-nonce set: re-burn every nonce
|
|
5382
|
+
* found in persisted raw events (any status - a nonce is burned the moment
|
|
5383
|
+
* its event entered the ledger's delegated path; fail-closed re-covers even
|
|
5384
|
+
* pre-mark rejects). Runs BEFORE `transport.start`.
|
|
5385
|
+
*/
|
|
5386
|
+
reconcileUsedNonces() {
|
|
5387
|
+
if (this.nonceStore === void 0) {
|
|
5388
|
+
return;
|
|
5389
|
+
}
|
|
5390
|
+
const nowSecs = Math.floor(Date.now() / 1e3);
|
|
5391
|
+
let changed = false;
|
|
5392
|
+
for (const entry of this.ledger.allEntries()) {
|
|
5393
|
+
if (entry.raw_event_json === void 0) {
|
|
5394
|
+
continue;
|
|
5395
|
+
}
|
|
5396
|
+
try {
|
|
5397
|
+
const parsed = parseDelegatedPayment(JSON.parse(entry.raw_event_json));
|
|
5398
|
+
if (parsed === null) {
|
|
5399
|
+
continue;
|
|
5400
|
+
}
|
|
5401
|
+
const retainUntil = parsed.expiryUnix + PROOF_CLOCK_SKEW_SECS;
|
|
5402
|
+
if (retainUntil <= nowSecs) {
|
|
5403
|
+
continue;
|
|
5404
|
+
}
|
|
5405
|
+
const key = `${parsed.owner}:${parsed.nonce}`;
|
|
5406
|
+
if (!this.nonceStore.has(key)) {
|
|
5407
|
+
this.nonceStore.markUsed(key, retainUntil, { persist: false });
|
|
5408
|
+
changed = true;
|
|
5409
|
+
}
|
|
5410
|
+
} catch {
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
if (changed) {
|
|
5414
|
+
try {
|
|
5415
|
+
this.nonceStore.flush();
|
|
5416
|
+
} catch {
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
/**
|
|
5421
|
+
* Delegated discriminator for recovery routing. The ledger flag is primary;
|
|
5422
|
+
* the persisted raw event's top-level `payment` tag is the fallback for an
|
|
5423
|
+
* entry whose flag write was lost.
|
|
5424
|
+
*/
|
|
5425
|
+
isDelegatedEntry(entry) {
|
|
5426
|
+
if (entry.delegated === true) {
|
|
5427
|
+
return true;
|
|
5428
|
+
}
|
|
5429
|
+
if (entry.raw_event_json === void 0) {
|
|
5430
|
+
return false;
|
|
5431
|
+
}
|
|
5432
|
+
try {
|
|
5433
|
+
const raw = JSON.parse(entry.raw_event_json);
|
|
5434
|
+
return Array.isArray(raw.tags) && raw.tags.some((tag) => Array.isArray(tag) && tag[0] === "payment" && tag[1] === "delegated");
|
|
5435
|
+
} catch {
|
|
5436
|
+
return false;
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
5439
|
+
/**
|
|
5440
|
+
* Reconcile a delegated `paid` entry to its terminal - the FIRST action in
|
|
5441
|
+
* the recovery `paid` branch, BEFORE skill.route/health/preflight gates: a
|
|
5442
|
+
* renamed skill or an unhealthy LLM must never strand an already-charged
|
|
5443
|
+
* pull, and redelivering a persisted result needs none of them. Operates
|
|
5444
|
+
* purely on persisted state - never re-executes, never re-signs.
|
|
5445
|
+
*/
|
|
5446
|
+
async reconcileDelegatedEntry(entry, fakeJob, log) {
|
|
5447
|
+
if (entry.pull_signature === void 0) {
|
|
5448
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: delegated entry has no pull, marking failed`);
|
|
5449
|
+
this.ledger.markFailed(entry.job_id);
|
|
5450
|
+
await this.transport.sendFeedback(fakeJob, {
|
|
5451
|
+
type: "error",
|
|
5452
|
+
message: `${DELEGATED_REJECTED_PREFIX}: the job did not complete - you were not charged. Please re-submit.`
|
|
5453
|
+
}).catch(() => {
|
|
5454
|
+
});
|
|
5455
|
+
return;
|
|
5456
|
+
}
|
|
5457
|
+
const rpc = createSolanaRpc(getRpcUrl(this.config.network));
|
|
5458
|
+
const pullSignature = signature(entry.pull_signature);
|
|
5459
|
+
let outcome;
|
|
5460
|
+
if (entry.pull_last_valid_block_height !== void 0) {
|
|
5461
|
+
outcome = await confirmPullToTerminal(
|
|
5462
|
+
rpc,
|
|
5463
|
+
pullSignature,
|
|
5464
|
+
BigInt(entry.pull_last_valid_block_height),
|
|
5465
|
+
{ recheckAttempts: RECONCILE_RECHECK_ATTEMPTS }
|
|
5466
|
+
);
|
|
5467
|
+
} else {
|
|
5468
|
+
outcome = await isDefinitelyUnpaid(rpc, pullSignature, {
|
|
5469
|
+
recheckAttempts: RECONCILE_RECHECK_ATTEMPTS
|
|
5470
|
+
}) ? "dead" : "assume-landed";
|
|
5471
|
+
}
|
|
5472
|
+
if (outcome === "unresolved") {
|
|
5473
|
+
log(
|
|
5474
|
+
`[${entry.job_id.slice(0, 8)}] Recovery: delegated pull still unresolved; retrying next tick`
|
|
5475
|
+
);
|
|
5476
|
+
return;
|
|
5477
|
+
}
|
|
5478
|
+
if (outcome === "dead") {
|
|
5479
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: delegated pull provably dead, marking failed`);
|
|
5480
|
+
this.ledger.markFailed(entry.job_id);
|
|
5481
|
+
await this.transport.sendFeedback(fakeJob, {
|
|
5482
|
+
type: "error",
|
|
5483
|
+
message: `${DELEGATED_REJECTED_PREFIX}: the delegated transfer did not land - you were not charged. Please re-submit.`
|
|
5484
|
+
}).catch(() => {
|
|
5485
|
+
});
|
|
5486
|
+
return;
|
|
5487
|
+
}
|
|
5488
|
+
let attachments;
|
|
5489
|
+
try {
|
|
5490
|
+
attachments = await this.reShareResultAttachments(entry);
|
|
5491
|
+
} catch {
|
|
5492
|
+
log(
|
|
5493
|
+
`[${entry.job_id.slice(0, 8)}] Recovery: delegated result blob unavailable, marking failed`
|
|
4125
5494
|
);
|
|
4126
|
-
this.
|
|
5495
|
+
this.ledger.markFailed(entry.job_id);
|
|
5496
|
+
await this.transport.sendFeedback(fakeJob, {
|
|
5497
|
+
type: "error",
|
|
5498
|
+
message: `Job permanently failed: the result could not be recovered after an agent restart. The delegated transfer landed (tx ${pullSignature}) - contact the provider to resolve.`
|
|
5499
|
+
}).catch(() => {
|
|
5500
|
+
});
|
|
5501
|
+
return;
|
|
4127
5502
|
}
|
|
4128
|
-
|
|
4129
|
-
await this.transport.
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
log(`[${job.jobId.slice(0, 8)}] Executing skill: ${skill.name}`);
|
|
4136
|
-
const budgetMs = this.resolveExecutionBudgetMs(skill);
|
|
4137
|
-
const jobSession = this.resolveJobSession(job, skill);
|
|
4138
|
-
const runExecution = async (history) => {
|
|
4139
|
-
let budgetExceeded = false;
|
|
4140
|
-
const execAbort = new AbortController();
|
|
4141
|
-
const onOuterAbort = () => execAbort.abort();
|
|
4142
|
-
if (signal) {
|
|
4143
|
-
if (signal.aborted) {
|
|
4144
|
-
execAbort.abort();
|
|
4145
|
-
} else {
|
|
4146
|
-
signal.addEventListener("abort", onOuterAbort);
|
|
4147
|
-
}
|
|
4148
|
-
}
|
|
4149
|
-
let budgetTimer;
|
|
4150
|
-
try {
|
|
4151
|
-
const execPromise = skill.execute(
|
|
4152
|
-
{
|
|
4153
|
-
data: inputFile?.inlineText ?? job.input,
|
|
4154
|
-
inputType: job.inputType,
|
|
4155
|
-
tags: job.tags,
|
|
4156
|
-
jobId: job.jobId,
|
|
4157
|
-
filePath: inputFile?.filePath,
|
|
4158
|
-
history,
|
|
4159
|
-
...jobSession !== null ? { sessionId: jobSession.sessionId } : {}
|
|
4160
|
-
},
|
|
4161
|
-
{ ...this.skillCtx, signal: execAbort.signal }
|
|
4162
|
-
);
|
|
4163
|
-
execPromise.catch(() => {
|
|
4164
|
-
});
|
|
4165
|
-
if (budgetMs > 0) {
|
|
4166
|
-
return await Promise.race([
|
|
4167
|
-
execPromise,
|
|
4168
|
-
new Promise((_resolve, reject) => {
|
|
4169
|
-
budgetTimer = setTimeout(() => {
|
|
4170
|
-
budgetExceeded = true;
|
|
4171
|
-
execAbort.abort();
|
|
4172
|
-
reject(new ExecutionBudgetExceededError(budgetMs));
|
|
4173
|
-
}, budgetMs);
|
|
4174
|
-
})
|
|
4175
|
-
]);
|
|
4176
|
-
}
|
|
4177
|
-
return await execPromise;
|
|
4178
|
-
} catch (err) {
|
|
4179
|
-
if (budgetExceeded) {
|
|
4180
|
-
log(`[${job.jobId.slice(0, 8)}] Execution exceeded budget (${budgetMs / 1e3}s)`);
|
|
4181
|
-
throw new ExecutionBudgetExceededError(budgetMs);
|
|
4182
|
-
}
|
|
4183
|
-
const flippedToUnhealthy = this.markHealthFromExecuteError(skill, err, log, job.jobId);
|
|
4184
|
-
if (flippedToUnhealthy) {
|
|
4185
|
-
throw new AgentUnavailableError();
|
|
4186
|
-
}
|
|
4187
|
-
throw err;
|
|
4188
|
-
} finally {
|
|
4189
|
-
if (budgetTimer) {
|
|
4190
|
-
clearTimeout(budgetTimer);
|
|
4191
|
-
}
|
|
4192
|
-
if (signal) {
|
|
4193
|
-
signal.removeEventListener("abort", onOuterAbort);
|
|
4194
|
-
}
|
|
4195
|
-
if (inputFile) {
|
|
4196
|
-
await inputFile.cleanup().catch(() => {
|
|
4197
|
-
});
|
|
4198
|
-
}
|
|
4199
|
-
}
|
|
4200
|
-
};
|
|
4201
|
-
const output = jobSession === null ? await runExecution() : await this.runSessionExchange({
|
|
4202
|
-
session: jobSession,
|
|
4203
|
-
jobId: job.jobId,
|
|
4204
|
-
userRecord: buildUserRecord(
|
|
4205
|
-
inputFile?.inlineText ?? job.input,
|
|
4206
|
-
inputFile?.filePath !== void 0 ? job.attachment?.name ?? "attachment" : void 0
|
|
4207
|
-
),
|
|
4208
|
-
signal,
|
|
4209
|
-
log,
|
|
4210
|
-
execute: runExecution
|
|
4211
|
-
});
|
|
4212
|
-
const { attachments, deliveredContent } = await this.buildResultAttachment(
|
|
4213
|
-
job.jobId,
|
|
4214
|
-
output,
|
|
4215
|
-
job.customerId,
|
|
4216
|
-
readAcceptedTransports(job.rawEvent.tags)
|
|
5503
|
+
this.ledger.markExecuted(entry.job_id, entry.delivered_content ?? "");
|
|
5504
|
+
await this.transport.deliverResult(
|
|
5505
|
+
fakeJob,
|
|
5506
|
+
entry.delivered_content ?? "",
|
|
5507
|
+
entry.net_amount,
|
|
5508
|
+
attachments.length > 0 ? attachments : void 0,
|
|
5509
|
+
entry.pull_signature
|
|
4217
5510
|
);
|
|
4218
|
-
this.ledger.
|
|
4219
|
-
log(`[${
|
|
4220
|
-
const eventId = await this.transport.deliverResult(
|
|
4221
|
-
job,
|
|
4222
|
-
deliveredContent,
|
|
4223
|
-
netAmount,
|
|
4224
|
-
attachments.length > 0 ? attachments : void 0
|
|
4225
|
-
);
|
|
4226
|
-
this.ledger.markDelivered(job.jobId);
|
|
4227
|
-
log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
|
|
4228
|
-
this.callbacks.onJobCompleted?.(job.jobId, output.data);
|
|
5511
|
+
this.ledger.markDelivered(entry.job_id);
|
|
5512
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: delegated pull ${outcome}, result delivered`);
|
|
4229
5513
|
}
|
|
4230
5514
|
/**
|
|
4231
5515
|
* Seed a result blob through iroh, timing it and converting any failure (incl. the
|
|
@@ -4792,7 +6076,8 @@ var AgentRuntime = class {
|
|
|
4792
6076
|
const ageMs = (Math.floor(Date.now() / 1e3) - entry.created_at) * 1e3;
|
|
4793
6077
|
const expired = ageMs > MAX_PAID_AGE_MS;
|
|
4794
6078
|
const exhaustedRetries = entry.status === "paid" && entry.retry_count >= this.config.recoveryMaxRetries;
|
|
4795
|
-
|
|
6079
|
+
const hasPullSignature = entry.pull_signature !== void 0;
|
|
6080
|
+
if ((expired || exhaustedRetries) && !hasPullSignature) {
|
|
4796
6081
|
this.ledger.markFailed(entry.job_id);
|
|
4797
6082
|
const reason = expired ? "Job permanently failed: agent did not recover within 24 hours" : "Job permanently failed after maximum retries";
|
|
4798
6083
|
if (entry.raw_event_json) {
|
|
@@ -4861,17 +6146,27 @@ var AgentRuntime = class {
|
|
|
4861
6146
|
} catch {
|
|
4862
6147
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: result blob unavailable, marking failed`);
|
|
4863
6148
|
this.ledger.markFailed(entry.job_id);
|
|
6149
|
+
await this.transport.sendFeedback(fakeJob, {
|
|
6150
|
+
type: "error",
|
|
6151
|
+
message: "Job permanently failed: the result data was lost after an agent restart - contact the provider to resolve."
|
|
6152
|
+
}).catch(() => {
|
|
6153
|
+
});
|
|
4864
6154
|
return;
|
|
4865
6155
|
}
|
|
4866
6156
|
await this.transport.deliverResult(
|
|
4867
6157
|
fakeJob,
|
|
4868
6158
|
entry.result,
|
|
4869
6159
|
entry.net_amount,
|
|
4870
|
-
attachments.length > 0 ? attachments : void 0
|
|
6160
|
+
attachments.length > 0 ? attachments : void 0,
|
|
6161
|
+
entry.pull_signature
|
|
4871
6162
|
);
|
|
4872
6163
|
this.ledger.markDelivered(entry.job_id);
|
|
4873
6164
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
|
|
4874
6165
|
} else if (entry.status === "paid") {
|
|
6166
|
+
if (this.isDelegatedEntry(entry)) {
|
|
6167
|
+
await this.reconcileDelegatedEntry(entry, fakeJob, log);
|
|
6168
|
+
return;
|
|
6169
|
+
}
|
|
4875
6170
|
const skill = this.skills.route(entry.tags);
|
|
4876
6171
|
if (!skill) {
|
|
4877
6172
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: no skill for tags, marking failed`);
|
|
@@ -5504,6 +6799,9 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
5504
6799
|
if (parsed.executionTimeoutSecs !== void 0) {
|
|
5505
6800
|
skill.executionTimeoutSecs = parsed.executionTimeoutSecs;
|
|
5506
6801
|
}
|
|
6802
|
+
if (parsed.delegation) {
|
|
6803
|
+
skill.delegation = parsed.delegation;
|
|
6804
|
+
}
|
|
5507
6805
|
return skill;
|
|
5508
6806
|
}
|
|
5509
6807
|
function loadSkillsFromDir(skillsDir, options = {}) {
|
|
@@ -5600,6 +6898,13 @@ var NostrTransport = class {
|
|
|
5600
6898
|
} catch {
|
|
5601
6899
|
return;
|
|
5602
6900
|
}
|
|
6901
|
+
let delegated;
|
|
6902
|
+
let delegatedRejected;
|
|
6903
|
+
try {
|
|
6904
|
+
delegated = parseDelegatedPayment(event) ?? void 0;
|
|
6905
|
+
} catch (error) {
|
|
6906
|
+
delegatedRejected = error instanceof Error ? error.message : String(error);
|
|
6907
|
+
}
|
|
5603
6908
|
onJob({
|
|
5604
6909
|
jobId: event.id,
|
|
5605
6910
|
input,
|
|
@@ -5610,7 +6915,9 @@ var NostrTransport = class {
|
|
|
5610
6915
|
encrypted,
|
|
5611
6916
|
rawEvent: event,
|
|
5612
6917
|
attachment,
|
|
5613
|
-
session
|
|
6918
|
+
session,
|
|
6919
|
+
delegated,
|
|
6920
|
+
delegatedRejected
|
|
5614
6921
|
});
|
|
5615
6922
|
}
|
|
5616
6923
|
);
|
|
@@ -5707,8 +7014,12 @@ var NostrTransport = class {
|
|
|
5707
7014
|
);
|
|
5708
7015
|
}
|
|
5709
7016
|
}
|
|
5710
|
-
/**
|
|
5711
|
-
|
|
7017
|
+
/**
|
|
7018
|
+
* Deliver result to customer. Retries with exponential backoff via SDK.
|
|
7019
|
+
* `paymentTx` attaches an on-chain settlement signature (the delegated pull)
|
|
7020
|
+
* to the result event for customer-side transparency.
|
|
7021
|
+
*/
|
|
7022
|
+
async deliverResult(job, content, amount, attachments, paymentTx, retries = 3) {
|
|
5712
7023
|
return this.client.marketplace.submitJobResultWithRetry(
|
|
5713
7024
|
this.identity,
|
|
5714
7025
|
job.rawEvent,
|
|
@@ -5716,7 +7027,8 @@ var NostrTransport = class {
|
|
|
5716
7027
|
amount,
|
|
5717
7028
|
retries,
|
|
5718
7029
|
void 0,
|
|
5719
|
-
attachments
|
|
7030
|
+
attachments,
|
|
7031
|
+
paymentTx !== void 0 ? { paymentTx } : void 0
|
|
5720
7032
|
);
|
|
5721
7033
|
}
|
|
5722
7034
|
/** Returns true if an event was received within the given idle window. */
|
|
@@ -5852,216 +7164,6 @@ function startWatchdog(deps) {
|
|
|
5852
7164
|
}
|
|
5853
7165
|
};
|
|
5854
7166
|
}
|
|
5855
|
-
|
|
5856
|
-
// src/x402/constants.ts
|
|
5857
|
-
var X402_SOLANA_DEVNET_CAIP2 = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
5858
|
-
var X402_SOLANA_DEVNET_V1 = "solana-devnet";
|
|
5859
|
-
var X402_GET_INPUT_MAX_ENCODED_BYTES = 2048;
|
|
5860
|
-
var X402_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
5861
|
-
var X402_MAX_CHALLENGE_BYTES = 256 * 1024;
|
|
5862
|
-
var X402_PROBE_TTL_MS = 3e4;
|
|
5863
|
-
var X402_MAX_PAID_ATTEMPTS = 2;
|
|
5864
|
-
var X402_MAX_PAYMENT_SIGNATURES = 2 * X402_MAX_PAID_ATTEMPTS;
|
|
5865
|
-
var X402_FREE_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
5866
|
-
var X402_REFUNDED_RETRIES = 1;
|
|
5867
|
-
var X402_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5868
|
-
function requirementAmount(requirement) {
|
|
5869
|
-
const v1Amount = requirement.maxAmountRequired;
|
|
5870
|
-
const raw = requirement.amount ?? v1Amount;
|
|
5871
|
-
if (typeof raw !== "string" || !/^[0-9]+$/.test(raw)) {
|
|
5872
|
-
return null;
|
|
5873
|
-
}
|
|
5874
|
-
return BigInt(raw);
|
|
5875
|
-
}
|
|
5876
|
-
function isAcceptableRequirement(requirement, rule) {
|
|
5877
|
-
if (requirement.scheme !== "exact") {
|
|
5878
|
-
return false;
|
|
5879
|
-
}
|
|
5880
|
-
const network = requirement.network;
|
|
5881
|
-
if (network !== X402_SOLANA_DEVNET_CAIP2 && network !== X402_SOLANA_DEVNET_V1) {
|
|
5882
|
-
return false;
|
|
5883
|
-
}
|
|
5884
|
-
if (USDC_SOLANA_DEVNET.mint === void 0 || requirement.asset !== USDC_SOLANA_DEVNET.mint) {
|
|
5885
|
-
return false;
|
|
5886
|
-
}
|
|
5887
|
-
const amount = requirementAmount(requirement);
|
|
5888
|
-
if (amount === null) {
|
|
5889
|
-
return false;
|
|
5890
|
-
}
|
|
5891
|
-
return amount <= rule.maxUpstreamSubunits;
|
|
5892
|
-
}
|
|
5893
|
-
function selectAcceptableRequirement(accepts, rule) {
|
|
5894
|
-
return accepts.find((requirement) => isAcceptableRequirement(requirement, rule));
|
|
5895
|
-
}
|
|
5896
|
-
function maxAcceptableQuote(accepts, rule) {
|
|
5897
|
-
let worst = null;
|
|
5898
|
-
for (const requirement of accepts) {
|
|
5899
|
-
if (!isAcceptableRequirement(requirement, rule)) {
|
|
5900
|
-
continue;
|
|
5901
|
-
}
|
|
5902
|
-
const amount = requirementAmount(requirement);
|
|
5903
|
-
if (amount === null) {
|
|
5904
|
-
continue;
|
|
5905
|
-
}
|
|
5906
|
-
if (worst === null || amount > worst) {
|
|
5907
|
-
worst = amount;
|
|
5908
|
-
}
|
|
5909
|
-
}
|
|
5910
|
-
return worst;
|
|
5911
|
-
}
|
|
5912
|
-
function buildRequirementsPolicy(rule) {
|
|
5913
|
-
return function filterAcceptableRequirements(_x402Version, paymentRequirements) {
|
|
5914
|
-
return paymentRequirements.filter((requirement) => isAcceptableRequirement(requirement, rule));
|
|
5915
|
-
};
|
|
5916
|
-
}
|
|
5917
|
-
var PROBE_TIMEOUT_MS = 1e4;
|
|
5918
|
-
var MAX_PROBE_BODY_BYTES = 256 * 1024;
|
|
5919
|
-
var PROBE_CACHE_MAX_ENTRIES = 256;
|
|
5920
|
-
async function readTextCapped(response, maxBytes) {
|
|
5921
|
-
const reader = response.body?.getReader();
|
|
5922
|
-
if (reader === void 0) {
|
|
5923
|
-
const buffer = await response.arrayBuffer();
|
|
5924
|
-
if (buffer.byteLength > maxBytes) {
|
|
5925
|
-
throw new X402ProbeError(`402 body exceeds ${maxBytes} bytes`, 402);
|
|
5926
|
-
}
|
|
5927
|
-
return new TextDecoder().decode(buffer);
|
|
5928
|
-
}
|
|
5929
|
-
const chunks = [];
|
|
5930
|
-
let total = 0;
|
|
5931
|
-
while (true) {
|
|
5932
|
-
const { done, value } = await reader.read();
|
|
5933
|
-
if (done) {
|
|
5934
|
-
break;
|
|
5935
|
-
}
|
|
5936
|
-
total += value.byteLength;
|
|
5937
|
-
if (total > maxBytes) {
|
|
5938
|
-
await reader.cancel().catch(() => {
|
|
5939
|
-
});
|
|
5940
|
-
throw new X402ProbeError(`402 body exceeds ${maxBytes} bytes`, 402);
|
|
5941
|
-
}
|
|
5942
|
-
chunks.push(value);
|
|
5943
|
-
}
|
|
5944
|
-
const combined = new Uint8Array(total);
|
|
5945
|
-
let offset = 0;
|
|
5946
|
-
for (const chunk of chunks) {
|
|
5947
|
-
combined.set(chunk, offset);
|
|
5948
|
-
offset += chunk.byteLength;
|
|
5949
|
-
}
|
|
5950
|
-
return new TextDecoder().decode(combined);
|
|
5951
|
-
}
|
|
5952
|
-
var X402ProbeError = class extends Error {
|
|
5953
|
-
status;
|
|
5954
|
-
constructor(message, status) {
|
|
5955
|
-
super(message);
|
|
5956
|
-
this.name = "X402ProbeError";
|
|
5957
|
-
this.status = status;
|
|
5958
|
-
}
|
|
5959
|
-
};
|
|
5960
|
-
function normalizeV1Requirement(raw) {
|
|
5961
|
-
return {
|
|
5962
|
-
scheme: typeof raw.scheme === "string" ? raw.scheme : "",
|
|
5963
|
-
network: typeof raw.network === "string" ? raw.network : "unknown:unknown",
|
|
5964
|
-
asset: typeof raw.asset === "string" ? raw.asset : "",
|
|
5965
|
-
payTo: typeof raw.payTo === "string" ? raw.payTo : "",
|
|
5966
|
-
amount: typeof raw.maxAmountRequired === "string" ? raw.maxAmountRequired : "",
|
|
5967
|
-
maxTimeoutSeconds: typeof raw.maxTimeoutSeconds === "number" ? raw.maxTimeoutSeconds : 60,
|
|
5968
|
-
extra: raw.extra ?? {}
|
|
5969
|
-
};
|
|
5970
|
-
}
|
|
5971
|
-
function parseV1Body(bodyText) {
|
|
5972
|
-
let parsed;
|
|
5973
|
-
try {
|
|
5974
|
-
parsed = JSON.parse(bodyText);
|
|
5975
|
-
} catch {
|
|
5976
|
-
return null;
|
|
5977
|
-
}
|
|
5978
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
5979
|
-
return null;
|
|
5980
|
-
}
|
|
5981
|
-
const body = parsed;
|
|
5982
|
-
if (body.x402Version !== 1 || !Array.isArray(body.accepts) || body.accepts.length === 0) {
|
|
5983
|
-
return null;
|
|
5984
|
-
}
|
|
5985
|
-
const rawRequirements = body.accepts.filter(
|
|
5986
|
-
(item) => typeof item === "object" && item !== null
|
|
5987
|
-
);
|
|
5988
|
-
if (rawRequirements.length === 0) {
|
|
5989
|
-
return null;
|
|
5990
|
-
}
|
|
5991
|
-
const accepts = rawRequirements.map(normalizeV1Requirement);
|
|
5992
|
-
const first = rawRequirements[0];
|
|
5993
|
-
return {
|
|
5994
|
-
x402Version: 1,
|
|
5995
|
-
accepts,
|
|
5996
|
-
resource: {
|
|
5997
|
-
url: typeof first.resource === "string" ? first.resource : void 0,
|
|
5998
|
-
description: typeof first.description === "string" ? first.description : void 0,
|
|
5999
|
-
mimeType: typeof first.mimeType === "string" ? first.mimeType : void 0
|
|
6000
|
-
}
|
|
6001
|
-
};
|
|
6002
|
-
}
|
|
6003
|
-
async function probePaymentRequired(url, method, signal) {
|
|
6004
|
-
const probeSignal = AbortSignal.timeout(PROBE_TIMEOUT_MS);
|
|
6005
|
-
const response = await fetch(url, { method, signal: probeSignal, redirect: "error" });
|
|
6006
|
-
if (response.status !== 402) {
|
|
6007
|
-
await response.body?.cancel().catch(() => {
|
|
6008
|
-
});
|
|
6009
|
-
throw new X402ProbeError(
|
|
6010
|
-
`expected HTTP 402 from ${url}, got ${response.status} - not an x402-paid endpoint (or it validates the request before the paywall; try the other method)`,
|
|
6011
|
-
response.status
|
|
6012
|
-
);
|
|
6013
|
-
}
|
|
6014
|
-
const headerValue = response.headers.get("PAYMENT-REQUIRED");
|
|
6015
|
-
if (headerValue !== null) {
|
|
6016
|
-
await response.body?.cancel().catch(() => {
|
|
6017
|
-
});
|
|
6018
|
-
try {
|
|
6019
|
-
const paymentRequired = decodePaymentRequiredHeader(headerValue);
|
|
6020
|
-
return {
|
|
6021
|
-
x402Version: paymentRequired.x402Version,
|
|
6022
|
-
accepts: paymentRequired.accepts,
|
|
6023
|
-
resource: paymentRequired.resource
|
|
6024
|
-
};
|
|
6025
|
-
} catch (error) {
|
|
6026
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6027
|
-
throw new X402ProbeError(`could not decode PAYMENT-REQUIRED header: ${message}`, 402);
|
|
6028
|
-
}
|
|
6029
|
-
}
|
|
6030
|
-
const bodyText = await readTextCapped(response, MAX_PROBE_BODY_BYTES).catch((error) => {
|
|
6031
|
-
if (error instanceof X402ProbeError) {
|
|
6032
|
-
throw error;
|
|
6033
|
-
}
|
|
6034
|
-
return "";
|
|
6035
|
-
});
|
|
6036
|
-
const v1 = parseV1Body(bodyText);
|
|
6037
|
-
if (v1 !== null) {
|
|
6038
|
-
return v1;
|
|
6039
|
-
}
|
|
6040
|
-
throw new X402ProbeError(
|
|
6041
|
-
"got HTTP 402 but neither a v2 PAYMENT-REQUIRED header nor a v1 JSON body was parseable",
|
|
6042
|
-
402
|
|
6043
|
-
);
|
|
6044
|
-
}
|
|
6045
|
-
var PROBE_CACHE = /* @__PURE__ */ new Map();
|
|
6046
|
-
async function probePaymentRequiredCached(url, method, ttlMs) {
|
|
6047
|
-
const key = `${method} ${url}`;
|
|
6048
|
-
const cached = PROBE_CACHE.get(key);
|
|
6049
|
-
const now = Date.now();
|
|
6050
|
-
if (cached !== void 0 && now - cached.at < ttlMs) {
|
|
6051
|
-
return cached.result;
|
|
6052
|
-
}
|
|
6053
|
-
const result = await probePaymentRequired(url, method);
|
|
6054
|
-
PROBE_CACHE.delete(key);
|
|
6055
|
-
PROBE_CACHE.set(key, { at: now, result });
|
|
6056
|
-
while (PROBE_CACHE.size > PROBE_CACHE_MAX_ENTRIES) {
|
|
6057
|
-
const oldest = PROBE_CACHE.keys().next().value;
|
|
6058
|
-
if (oldest === void 0) {
|
|
6059
|
-
break;
|
|
6060
|
-
}
|
|
6061
|
-
PROBE_CACHE.delete(oldest);
|
|
6062
|
-
}
|
|
6063
|
-
return result;
|
|
6064
|
-
}
|
|
6065
7167
|
var X402_JOBS_FILE = ".x402-jobs.json";
|
|
6066
7168
|
var X402_RESULTS_DIR = ".x402-results";
|
|
6067
7169
|
function sanitizeJobId(jobId) {
|
|
@@ -6691,18 +7793,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
6691
7793
|
`);
|
|
6692
7794
|
}
|
|
6693
7795
|
}
|
|
6694
|
-
const scriptEnv =
|
|
6695
|
-
delete scriptEnv.ELISYM_PASSPHRASE;
|
|
6696
|
-
if (scriptEnv.SOLANA_RPC_URL !== void 0 && !isPublicSolanaRpcUrl(scriptEnv.SOLANA_RPC_URL)) {
|
|
6697
|
-
delete scriptEnv.SOLANA_RPC_URL;
|
|
6698
|
-
}
|
|
6699
|
-
const llmKeys = loaded.secrets.llm_api_keys ?? {};
|
|
6700
|
-
for (const descriptor of listLlmProviders()) {
|
|
6701
|
-
const secretValue = llmKeys[descriptor.id];
|
|
6702
|
-
if (typeof secretValue === "string" && secretValue.length > 0) {
|
|
6703
|
-
scriptEnv[descriptor.envVar] = secretValue;
|
|
6704
|
-
}
|
|
6705
|
-
}
|
|
7796
|
+
const scriptEnv = buildScriptEnv(loaded.secrets);
|
|
6706
7797
|
const paths = agentPaths(loaded.dir);
|
|
6707
7798
|
const skillsDir = paths.skills;
|
|
6708
7799
|
const allSkills = loadSkillsFromDir(skillsDir, { scriptEnv });
|
|
@@ -7122,6 +8213,46 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
7122
8213
|
}
|
|
7123
8214
|
}
|
|
7124
8215
|
const kinds = [jobRequestKind(DEFAULT_KIND_OFFSET)];
|
|
8216
|
+
const declaresDelegation = allSkills.some((skill) => skill.delegation !== void 0);
|
|
8217
|
+
let delegatePubkey;
|
|
8218
|
+
let delegateSigner;
|
|
8219
|
+
if (declaresDelegation && walletNetwork !== "devnet") {
|
|
8220
|
+
console.warn(
|
|
8221
|
+
` ! A skill declares delegation but it is devnet-only today (network: ${walletNetwork}) - omitted from its card(s).`
|
|
8222
|
+
);
|
|
8223
|
+
} else if (declaresDelegation) {
|
|
8224
|
+
const delegateSecret = loaded.secrets.solana_delegate_secret_key;
|
|
8225
|
+
if (delegateSecret && delegateSecret.length > 0) {
|
|
8226
|
+
try {
|
|
8227
|
+
delegateSigner = await signerFromSecretKeyBase58(delegateSecret);
|
|
8228
|
+
delegatePubkey = delegateSigner.address;
|
|
8229
|
+
console.log(` Delegate (spl-approve) ${delegatePubkey}`);
|
|
8230
|
+
try {
|
|
8231
|
+
const { value: delegateLamports } = await createSolanaRpc(getRpcUrl(walletNetwork)).getBalance(address(delegatePubkey)).send();
|
|
8232
|
+
if (delegateLamports === 0n) {
|
|
8233
|
+
console.warn(
|
|
8234
|
+
" ! Delegate key holds no SOL - delegated job pulls will fail on tx fees. Fund it: https://faucet.solana.com"
|
|
8235
|
+
);
|
|
8236
|
+
}
|
|
8237
|
+
} catch {
|
|
8238
|
+
}
|
|
8239
|
+
} catch (error) {
|
|
8240
|
+
delegateSigner = void 0;
|
|
8241
|
+
delegatePubkey = void 0;
|
|
8242
|
+
console.warn(
|
|
8243
|
+
` ! Delegate key is present but unreadable - the delegation field is omitted from its card(s). ${error instanceof Error ? error.message : String(error)}`
|
|
8244
|
+
);
|
|
8245
|
+
console.warn(
|
|
8246
|
+
` ! Run \`npx @elisym/cli delegate-key ${loaded.name} --rotate\` to replace it.`
|
|
8247
|
+
);
|
|
8248
|
+
}
|
|
8249
|
+
} else {
|
|
8250
|
+
console.warn(
|
|
8251
|
+
" ! A skill declares delegation but this agent has no delegate key - the delegation field is omitted from its card(s)."
|
|
8252
|
+
);
|
|
8253
|
+
console.warn(` ! Run \`npx @elisym/cli delegate-key ${loaded.name}\` to enable it.`);
|
|
8254
|
+
}
|
|
8255
|
+
}
|
|
7125
8256
|
function buildCard(skill) {
|
|
7126
8257
|
const isStatic = skill.mode === "static-file" || skill.mode === "static-script" || skill.noInput === true;
|
|
7127
8258
|
return {
|
|
@@ -7138,6 +8269,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
7138
8269
|
// Conversation-context flag: clients gate chat affordances (session-carrying
|
|
7139
8270
|
// sends) on it. Strict true only - the read side coerces anything else away.
|
|
7140
8271
|
...skill.context === true ? { context: true } : {},
|
|
8272
|
+
// Delegation descriptor: only when the skill opts in AND a delegate key
|
|
8273
|
+
// exists (delegatePubkey resolved). The pubkey is injected here, never
|
|
8274
|
+
// taken from the SKILL.md - the operator declares only the cap/mechanism.
|
|
8275
|
+
...skill.delegation && delegatePubkey ? { delegation: { ...skill.delegation, delegate_pubkey: delegatePubkey } } : {},
|
|
7141
8276
|
payment: solanaAddress ? {
|
|
7142
8277
|
chain: "solana",
|
|
7143
8278
|
network: walletNetwork,
|
|
@@ -7247,8 +8382,15 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
7247
8382
|
solanaAddress,
|
|
7248
8383
|
// Agent-level default execution budget; per-skill `max_execution_secs`
|
|
7249
8384
|
// overrides it. Undefined => unlimited (operator-owned, no protocol default).
|
|
7250
|
-
executionTimeoutSecs: loaded.yaml.execution_timeout_secs
|
|
8385
|
+
executionTimeoutSecs: loaded.yaml.execution_timeout_secs,
|
|
8386
|
+
// Delegated job payment: the runtime holds ONLY the delegate signer (never
|
|
8387
|
+
// the payment key) to execute post-work pulls from customer delegations.
|
|
8388
|
+
delegateSigner
|
|
7251
8389
|
};
|
|
8390
|
+
const nonceStore = delegateSigner !== void 0 ? new UsedNonceStore(join(loaded.dir, ".delegation-nonces.json")) : void 0;
|
|
8391
|
+
if (nonceStore !== void 0) {
|
|
8392
|
+
await ensureGitignoreHasDelegationNoncesEntry(dirname(loaded.dir));
|
|
8393
|
+
}
|
|
7252
8394
|
const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl());
|
|
7253
8395
|
logger.debug(
|
|
7254
8396
|
{
|
|
@@ -7321,11 +8463,27 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
7321
8463
|
irohTransport,
|
|
7322
8464
|
identity2,
|
|
7323
8465
|
blossomTransport,
|
|
7324
|
-
sessionStore
|
|
8466
|
+
sessionStore,
|
|
8467
|
+
nonceStore
|
|
7325
8468
|
);
|
|
7326
8469
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
7327
8470
|
await runtime.run();
|
|
7328
8471
|
}
|
|
8472
|
+
function buildScriptEnv(secrets) {
|
|
8473
|
+
const scriptEnv = { ...process.env };
|
|
8474
|
+
delete scriptEnv.ELISYM_PASSPHRASE;
|
|
8475
|
+
if (scriptEnv.SOLANA_RPC_URL !== void 0 && !isPublicSolanaRpcUrl(scriptEnv.SOLANA_RPC_URL)) {
|
|
8476
|
+
delete scriptEnv.SOLANA_RPC_URL;
|
|
8477
|
+
}
|
|
8478
|
+
const llmKeys = secrets.llm_api_keys ?? {};
|
|
8479
|
+
for (const descriptor of listLlmProviders()) {
|
|
8480
|
+
const secretValue = llmKeys[descriptor.id];
|
|
8481
|
+
if (typeof secretValue === "string" && secretValue.length > 0) {
|
|
8482
|
+
scriptEnv[descriptor.envVar] = secretValue;
|
|
8483
|
+
}
|
|
8484
|
+
}
|
|
8485
|
+
return scriptEnv;
|
|
8486
|
+
}
|
|
7329
8487
|
var PUBLIC_SOLANA_RPC_HOSTS = /* @__PURE__ */ new Set([
|
|
7330
8488
|
"api.devnet.solana.com",
|
|
7331
8489
|
"api.mainnet-beta.solana.com",
|
|
@@ -7502,468 +8660,43 @@ async function loadAgentWithPrompt(name, cwd) {
|
|
|
7502
8660
|
async function cmdWallet(name) {
|
|
7503
8661
|
const cwd = process.cwd();
|
|
7504
8662
|
if (!name) {
|
|
7505
|
-
const agents = await listAgents(cwd);
|
|
7506
|
-
if (agents.length === 0) {
|
|
7507
|
-
console.error("No agents found.");
|
|
7508
|
-
process.exit(1);
|
|
7509
|
-
}
|
|
7510
|
-
const { default: inquirer } = await import('inquirer');
|
|
7511
|
-
const { selected } = await inquirer.prompt([
|
|
7512
|
-
{
|
|
7513
|
-
type: "list",
|
|
7514
|
-
name: "selected",
|
|
7515
|
-
message: "Select agent:",
|
|
7516
|
-
choices: agents.map((agent) => ({
|
|
7517
|
-
name: `${agent.name} (${agent.source})`,
|
|
7518
|
-
value: agent.name
|
|
7519
|
-
}))
|
|
7520
|
-
}
|
|
7521
|
-
]);
|
|
7522
|
-
name = selected;
|
|
7523
|
-
}
|
|
7524
|
-
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
7525
|
-
const loaded = await loadAgent(name, cwd, passphrase);
|
|
7526
|
-
const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
|
|
7527
|
-
if (!solPayment?.address) {
|
|
7528
|
-
console.error("Solana address not configured for this agent.");
|
|
7529
|
-
process.exit(1);
|
|
7530
|
-
}
|
|
7531
|
-
const rpcUrl = getRpcUrl(solPayment.network);
|
|
7532
|
-
const rpc = createSolanaRpc(rpcUrl);
|
|
7533
|
-
const walletAddress = address(solPayment.address);
|
|
7534
|
-
const { value: balance } = await rpc.getBalance(walletAddress).send();
|
|
7535
|
-
const usdcBalance = await fetchUsdcBalance(rpc, walletAddress);
|
|
7536
|
-
console.log(`
|
|
7537
|
-
Agent: ${name}`);
|
|
7538
|
-
console.log(` Network: ${solPayment.network}`);
|
|
7539
|
-
console.log(` Address: ${solPayment.address}`);
|
|
7540
|
-
console.log(` SOL balance: ${formatSol(Number(balance))} (${balance} lamports)`);
|
|
7541
|
-
console.log(` USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}
|
|
7542
|
-
`);
|
|
7543
|
-
}
|
|
7544
|
-
var BPS_DENOMINATOR = 1e4;
|
|
7545
|
-
var DEFAULT_MARGIN_BPS = 1e3;
|
|
7546
|
-
var SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
7547
|
-
var CIRCLE_FAUCET_URL = "https://faucet.circle.com";
|
|
7548
|
-
function fail(message) {
|
|
7549
|
-
console.error(`
|
|
7550
|
-
! ${message}
|
|
7551
|
-
`);
|
|
7552
|
-
process.exit(1);
|
|
7553
|
-
}
|
|
7554
|
-
function sanitizeUpstreamText(raw, maxLength) {
|
|
7555
|
-
let withoutControls = "";
|
|
7556
|
-
for (const char of raw) {
|
|
7557
|
-
const code = char.codePointAt(0) ?? 0;
|
|
7558
|
-
withoutControls += code < 32 || code === 127 ? " " : char;
|
|
7559
|
-
}
|
|
7560
|
-
return withoutControls.replace(/—/g, "-").replace(/\s+/g, " ").trim().slice(0, maxLength).trim();
|
|
7561
|
-
}
|
|
7562
|
-
function slugFromUrl(url) {
|
|
7563
|
-
const pathPart = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? "";
|
|
7564
|
-
const host = url.hostname.replace(/^www\./, "").split(".")[0] ?? "x402";
|
|
7565
|
-
return pathPart.length > 0 ? `${host}-${pathPart}` : host;
|
|
7566
|
-
}
|
|
7567
|
-
function parseMarginBps(raw) {
|
|
7568
|
-
if (raw === void 0) {
|
|
7569
|
-
return DEFAULT_MARGIN_BPS;
|
|
7570
|
-
}
|
|
7571
|
-
const value = Number(raw);
|
|
7572
|
-
if (!Number.isInteger(value) || value < 0 || value > 1e5) {
|
|
7573
|
-
fail(`--margin-bps must be a non-negative integer (bps, 100 = 1%); got "${raw}"`);
|
|
7574
|
-
}
|
|
7575
|
-
return value;
|
|
7576
|
-
}
|
|
7577
|
-
async function parseImportedSecret(input) {
|
|
7578
|
-
const trimmed = input.trim();
|
|
7579
|
-
if (existsSync(trimmed)) {
|
|
7580
|
-
const content = await readFile(trimmed, "utf-8");
|
|
7581
|
-
let parsed;
|
|
7582
|
-
try {
|
|
7583
|
-
parsed = JSON.parse(content);
|
|
7584
|
-
} catch {
|
|
7585
|
-
throw new Error(`${trimmed} is not a valid solana-keygen JSON file`);
|
|
7586
|
-
}
|
|
7587
|
-
if (!Array.isArray(parsed) || parsed.length !== 64 || parsed.some(
|
|
7588
|
-
(byte) => typeof byte !== "number" || !Number.isInteger(byte) || byte < 0 || byte > 255
|
|
7589
|
-
)) {
|
|
7590
|
-
throw new Error(`${trimmed} must contain a JSON array of 64 bytes (solana-keygen format)`);
|
|
7591
|
-
}
|
|
7592
|
-
return encodeSecretKeyBase58(new Uint8Array(parsed));
|
|
7593
|
-
}
|
|
7594
|
-
try {
|
|
7595
|
-
await signerFromSecretKeyBase58(trimmed);
|
|
7596
|
-
} catch {
|
|
7597
|
-
throw new Error("not a readable file and not a valid base58-encoded 64-byte secret key");
|
|
7598
|
-
}
|
|
7599
|
-
return trimmed;
|
|
7600
|
-
}
|
|
7601
|
-
function computeBridgePriceSubunits(quote, marginBps, feeBps) {
|
|
7602
|
-
if (feeBps >= BPS_DENOMINATOR) {
|
|
7603
|
-
throw new Error(`feeBps ${feeBps} >= ${BPS_DENOMINATOR} - refusing zero/negative denominator`);
|
|
7604
|
-
}
|
|
7605
|
-
return BigInt(
|
|
7606
|
-
new Decimal(quote.toString()).mul(BPS_DENOMINATOR + marginBps).div(BPS_DENOMINATOR - feeBps).toDecimalPlaces(0, Decimal.ROUND_CEIL).toString()
|
|
7607
|
-
);
|
|
7608
|
-
}
|
|
7609
|
-
function buildSkillMd(input) {
|
|
7610
|
-
const frontmatter = {
|
|
7611
|
-
name: input.skillName,
|
|
7612
|
-
description: input.description,
|
|
7613
|
-
capabilities: input.capabilities,
|
|
7614
|
-
price: input.priceDisplay,
|
|
7615
|
-
token: "usdc",
|
|
7616
|
-
mode: "x402",
|
|
7617
|
-
x402_url: input.url,
|
|
7618
|
-
x402_method: input.method,
|
|
7619
|
-
...input.queryParam !== void 0 ? { x402_query_param: input.queryParam } : {},
|
|
7620
|
-
x402_max_upstream: Number(input.quote),
|
|
7621
|
-
// GET input is dominated by the ~2KB percent-encoded query-string limit,
|
|
7622
|
-
// so the byte cap is written for POST bridges only.
|
|
7623
|
-
...input.method === "POST" ? { x402_max_input_bytes: DEFAULT_X402_MAX_INPUT_BYTES } : {}
|
|
7624
|
-
};
|
|
7625
|
-
const bodyLines = [
|
|
7626
|
-
`# ${input.skillName}`,
|
|
7627
|
-
"",
|
|
7628
|
-
"Operator notes for this x402 bridge skill (the executor ignores this body):",
|
|
7629
|
-
"",
|
|
7630
|
-
`- Upstream: ${input.url} (${input.method})`,
|
|
7631
|
-
`- Upstream quote at generation time: ${formatAssetAmount(USDC_SOLANA_DEVNET, input.quote)} (= x402_max_upstream, the signing ceiling)`,
|
|
7632
|
-
`- Price: ${input.priceDisplay} USDC (margin ${input.marginPercent}%, protocol fee ${input.feePercent}% at generation time)`,
|
|
7633
|
-
"- To reprice after an upstream change, re-run:",
|
|
7634
|
-
"",
|
|
7635
|
-
"```bash",
|
|
7636
|
-
`npx @elisym/cli x402 add ${input.url}`,
|
|
7637
|
-
"```"
|
|
7638
|
-
];
|
|
7639
|
-
return `---
|
|
7640
|
-
${YAML.stringify(frontmatter)}---
|
|
7641
|
-
|
|
7642
|
-
${bodyLines.join("\n")}
|
|
7643
|
-
`;
|
|
7644
|
-
}
|
|
7645
|
-
function scanExistingSkills(skillsDir, candidateDTag, url) {
|
|
7646
|
-
const scan = {};
|
|
7647
|
-
let entries;
|
|
7648
|
-
try {
|
|
7649
|
-
entries = readdirSync(skillsDir);
|
|
7650
|
-
} catch {
|
|
7651
|
-
return scan;
|
|
7652
|
-
}
|
|
7653
|
-
for (const entry of entries) {
|
|
7654
|
-
const skillMdPath = join(skillsDir, entry, "SKILL.md");
|
|
7655
|
-
try {
|
|
7656
|
-
if (!statSync(join(skillsDir, entry)).isDirectory()) {
|
|
7657
|
-
continue;
|
|
7658
|
-
}
|
|
7659
|
-
const { frontmatter } = parseSkillMd(readFileSync(skillMdPath, "utf-8"));
|
|
7660
|
-
if (typeof frontmatter.name !== "string") {
|
|
7661
|
-
continue;
|
|
7662
|
-
}
|
|
7663
|
-
if (typeof frontmatter.x402_url === "string" && frontmatter.x402_url === url) {
|
|
7664
|
-
scan.sameUrlDir = join(skillsDir, entry);
|
|
7665
|
-
scan.sameUrlName = frontmatter.name;
|
|
7666
|
-
continue;
|
|
7667
|
-
}
|
|
7668
|
-
let existingDTag;
|
|
7669
|
-
try {
|
|
7670
|
-
existingDTag = toDTag(frontmatter.name);
|
|
7671
|
-
} catch {
|
|
7672
|
-
continue;
|
|
7673
|
-
}
|
|
7674
|
-
if (existingDTag === candidateDTag) {
|
|
7675
|
-
scan.dTagCollision = frontmatter.name;
|
|
7676
|
-
}
|
|
7677
|
-
} catch {
|
|
7678
|
-
continue;
|
|
7679
|
-
}
|
|
7680
|
-
}
|
|
7681
|
-
return scan;
|
|
7682
|
-
}
|
|
7683
|
-
function describeUnacceptableAccepts(probe) {
|
|
7684
|
-
const networks = [...new Set(probe.accepts.map((requirement) => String(requirement.network)))];
|
|
7685
|
-
if (networks.length > 0 && networks.every((network) => network.startsWith("eip155:"))) {
|
|
7686
|
-
return `the service accepts only EVM networks (${networks.join(", ")}) - not supported yet`;
|
|
7687
|
-
}
|
|
7688
|
-
if (networks.some((network) => network === SOLANA_MAINNET_CAIP2 || network === "solana")) {
|
|
7689
|
-
return "the service settles on Solana mainnet - elisym currently runs on devnet (mainnet is on the roadmap)";
|
|
7690
|
-
}
|
|
7691
|
-
return `no exact-scheme devnet-USDC requirement found (service accepts: ${networks.join(", ") || "nothing parseable"})`;
|
|
7692
|
-
}
|
|
7693
|
-
async function cmdX402Add(url, agentName, options) {
|
|
7694
|
-
const cwd = process.cwd();
|
|
7695
|
-
const { default: inquirer } = await import('inquirer');
|
|
7696
|
-
let parsedUrl;
|
|
7697
|
-
try {
|
|
7698
|
-
parsedUrl = new URL(url);
|
|
7699
|
-
} catch {
|
|
7700
|
-
fail(`"${url}" is not a valid URL`);
|
|
7701
|
-
}
|
|
7702
|
-
const isLoopback = ["localhost", "127.0.0.1", "[::1]"].includes(parsedUrl.hostname);
|
|
7703
|
-
if (parsedUrl.protocol !== "https:" && !(parsedUrl.protocol === "http:" && isLoopback)) {
|
|
7704
|
-
fail("the upstream must be an https:// URL (plain http is allowed for localhost only)");
|
|
7705
|
-
}
|
|
7706
|
-
const methodRaw = (options.method ?? "POST").toUpperCase();
|
|
7707
|
-
if (methodRaw !== "GET" && methodRaw !== "POST") {
|
|
7708
|
-
fail(`--method must be GET or POST; got "${options.method}"`);
|
|
7709
|
-
}
|
|
7710
|
-
const method = methodRaw;
|
|
7711
|
-
const marginBps = parseMarginBps(options.marginBps);
|
|
7712
|
-
if (!agentName) {
|
|
7713
|
-
const agents = await listAgents(cwd);
|
|
7714
|
-
if (agents.length === 0) {
|
|
7715
|
-
fail("no agents found - create one first: npx @elisym/cli init");
|
|
7716
|
-
}
|
|
7717
|
-
if (options.yes) {
|
|
7718
|
-
fail("--yes needs an explicit agent argument: elisym x402 add <url> <agent> --yes");
|
|
8663
|
+
const agents = await listAgents(cwd);
|
|
8664
|
+
if (agents.length === 0) {
|
|
8665
|
+
console.error("No agents found.");
|
|
8666
|
+
process.exit(1);
|
|
7719
8667
|
}
|
|
8668
|
+
const { default: inquirer } = await import('inquirer');
|
|
7720
8669
|
const { selected } = await inquirer.prompt([
|
|
7721
8670
|
{
|
|
7722
8671
|
type: "list",
|
|
7723
8672
|
name: "selected",
|
|
7724
|
-
message: "
|
|
8673
|
+
message: "Select agent:",
|
|
7725
8674
|
choices: agents.map((agent) => ({
|
|
7726
8675
|
name: `${agent.name} (${agent.source})`,
|
|
7727
8676
|
value: agent.name
|
|
7728
8677
|
}))
|
|
7729
8678
|
}
|
|
7730
8679
|
]);
|
|
7731
|
-
|
|
8680
|
+
name = selected;
|
|
7732
8681
|
}
|
|
7733
8682
|
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
7734
|
-
const loaded = await loadAgent(
|
|
7735
|
-
console.log(`
|
|
7736
|
-
Probing ${url} (${method}, unpaid)...`);
|
|
7737
|
-
let probe;
|
|
7738
|
-
try {
|
|
7739
|
-
probe = await probePaymentRequired(url, method);
|
|
7740
|
-
} catch (error) {
|
|
7741
|
-
if (error instanceof X402ProbeError) {
|
|
7742
|
-
fail(error.message);
|
|
7743
|
-
}
|
|
7744
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7745
|
-
fail(`upstream unreachable: ${message}`);
|
|
7746
|
-
}
|
|
7747
|
-
const anyCeiling = { maxUpstreamSubunits: BigInt(Number.MAX_SAFE_INTEGER) };
|
|
7748
|
-
const requirement = selectAcceptableRequirement(probe.accepts, anyCeiling);
|
|
7749
|
-
if (requirement === void 0) {
|
|
7750
|
-
fail(describeUnacceptableAccepts(probe));
|
|
7751
|
-
}
|
|
7752
|
-
const quote = requirementAmount(requirement);
|
|
7753
|
-
if (quote === null || quote <= 0n) {
|
|
7754
|
-
fail("the upstream quote is not a positive integer amount");
|
|
7755
|
-
}
|
|
7756
|
-
let solanaSecretKey = loaded.secrets.solana_secret_key;
|
|
7757
|
-
if (solanaSecretKey === void 0 || solanaSecretKey.length === 0) {
|
|
7758
|
-
let choice;
|
|
7759
|
-
if (options.yes || options.generateWallet) {
|
|
7760
|
-
if (!options.generateWallet) {
|
|
7761
|
-
fail(
|
|
7762
|
-
"this agent has no Solana wallet key. Run interactively to generate/import one, or pass --generate-wallet (a money-holding key is never created silently under --yes)."
|
|
7763
|
-
);
|
|
7764
|
-
}
|
|
7765
|
-
choice = "generate";
|
|
7766
|
-
} else {
|
|
7767
|
-
const { walletChoice } = await inquirer.prompt([
|
|
7768
|
-
{
|
|
7769
|
-
type: "list",
|
|
7770
|
-
name: "walletChoice",
|
|
7771
|
-
message: "The bridge pays the upstream from the agent wallet, but .secrets.json has no solana_secret_key:",
|
|
7772
|
-
choices: [
|
|
7773
|
-
{ name: "Generate a new wallet", value: "generate" },
|
|
7774
|
-
{ name: "Import an existing one (solana-keygen JSON file or base58)", value: "import" }
|
|
7775
|
-
]
|
|
7776
|
-
}
|
|
7777
|
-
]);
|
|
7778
|
-
choice = walletChoice;
|
|
7779
|
-
}
|
|
7780
|
-
if (choice === "generate") {
|
|
7781
|
-
const wallet = await generateSolanaWallet();
|
|
7782
|
-
solanaSecretKey = wallet.secretKeyBase58;
|
|
7783
|
-
} else {
|
|
7784
|
-
const { imported } = await inquirer.prompt([
|
|
7785
|
-
{
|
|
7786
|
-
// Masked: a pasted base58 secret key must not echo to the terminal
|
|
7787
|
-
// scrollback (matches every other secret prompt in the CLI). A file
|
|
7788
|
-
// path can still be pasted/drag-dropped under the mask.
|
|
7789
|
-
type: "password",
|
|
7790
|
-
mask: "*",
|
|
7791
|
-
name: "imported",
|
|
7792
|
-
message: "Path to a solana-keygen JSON file (or paste a base58 secret key):"
|
|
7793
|
-
}
|
|
7794
|
-
]);
|
|
7795
|
-
try {
|
|
7796
|
-
solanaSecretKey = await parseImportedSecret(imported);
|
|
7797
|
-
} catch (error) {
|
|
7798
|
-
fail(error instanceof Error ? error.message : String(error));
|
|
7799
|
-
}
|
|
7800
|
-
}
|
|
7801
|
-
await writeSecrets(
|
|
7802
|
-
loaded.dir,
|
|
7803
|
-
{ ...loaded.secrets, solana_secret_key: solanaSecretKey },
|
|
7804
|
-
passphrase
|
|
7805
|
-
);
|
|
7806
|
-
console.log(" Wallet key saved to .secrets.json");
|
|
7807
|
-
}
|
|
7808
|
-
const signer = await signerFromSecretKeyBase58(solanaSecretKey);
|
|
8683
|
+
const loaded = await loadAgent(name, cwd, passphrase);
|
|
7809
8684
|
const solPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
|
|
7810
|
-
if (solPayment
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
Revenue must land in the wallet the bridge spends from, or the float never refills.
|
|
7814
|
-
Fix: set payments[].address to the key address, or import the matching key.`
|
|
7815
|
-
);
|
|
7816
|
-
}
|
|
7817
|
-
if (solPayment === void 0) {
|
|
7818
|
-
console.log(
|
|
7819
|
-
` elisym.yaml has no Solana payments entry; adding address ${signer.address} (devnet).`
|
|
7820
|
-
);
|
|
7821
|
-
await writeYaml(loaded.dir, {
|
|
7822
|
-
...loaded.yaml,
|
|
7823
|
-
payments: [
|
|
7824
|
-
...loaded.yaml.payments,
|
|
7825
|
-
{ chain: "solana", network: "devnet", address: signer.address }
|
|
7826
|
-
]
|
|
7827
|
-
});
|
|
7828
|
-
}
|
|
7829
|
-
const rpc = createSolanaRpc(getRpcUrl());
|
|
7830
|
-
const balance = await fetchUsdcBalance(rpc, address(signer.address));
|
|
7831
|
-
let feeBps;
|
|
7832
|
-
try {
|
|
7833
|
-
const protocolConfig = await getProtocolConfig(rpc, getProtocolProgramId("devnet"), {
|
|
7834
|
-
forceRefresh: true
|
|
7835
|
-
});
|
|
7836
|
-
feeBps = protocolConfig.feeBps;
|
|
7837
|
-
} catch (error) {
|
|
7838
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7839
|
-
fail(`could not read the on-chain protocol config (fee): ${message}`);
|
|
7840
|
-
}
|
|
7841
|
-
if (feeBps >= BPS_DENOMINATOR) {
|
|
7842
|
-
fail(
|
|
7843
|
-
`protocol fee is ${feeBps} bps (>= 100%) - price math would divide by zero or go negative`
|
|
7844
|
-
);
|
|
7845
|
-
}
|
|
7846
|
-
const priceSubunits = computeBridgePriceSubunits(quote, marginBps, feeBps);
|
|
7847
|
-
const priceDisplay = new Decimal(priceSubunits.toString()).div(new Decimal(10).pow(USDC_SOLANA_DEVNET.decimals)).toString();
|
|
7848
|
-
const protocolFee = calculateProtocolFee(Number(priceSubunits), feeBps);
|
|
7849
|
-
const netMargin = priceSubunits - BigInt(protocolFee) - quote;
|
|
7850
|
-
const fallbackName = slugFromUrl(parsedUrl);
|
|
7851
|
-
const rawName = options.name ?? probe.resource?.serviceName ?? fallbackName;
|
|
7852
|
-
const skillName = sanitizeUpstreamText(rawName, LIMITS.MAX_AGENT_NAME_LENGTH);
|
|
7853
|
-
if (!/[a-zA-Z0-9]/.test(skillName)) {
|
|
7854
|
-
fail(
|
|
7855
|
-
`the upstream service name ("${rawName}") has no ASCII letters or digits - pass an explicit --name <skill-name>`
|
|
7856
|
-
);
|
|
7857
|
-
}
|
|
7858
|
-
const candidateDTag = toDTag(skillName);
|
|
7859
|
-
const description = sanitizeUpstreamText(
|
|
7860
|
-
probe.resource?.description ?? `x402 bridge to ${parsedUrl.hostname}`,
|
|
7861
|
-
LIMITS.MAX_DESCRIPTION_LENGTH
|
|
7862
|
-
);
|
|
7863
|
-
const capabilities = [candidateDTag];
|
|
7864
|
-
const skillsDir = join(loaded.dir, "skills");
|
|
7865
|
-
const scan = scanExistingSkills(skillsDir, candidateDTag, url);
|
|
7866
|
-
let targetDir = join(skillsDir, candidateDTag);
|
|
7867
|
-
if (scan.sameUrlDir !== void 0) {
|
|
7868
|
-
if (!options.yes) {
|
|
7869
|
-
const { update } = await inquirer.prompt([
|
|
7870
|
-
{
|
|
7871
|
-
type: "confirm",
|
|
7872
|
-
name: "update",
|
|
7873
|
-
message: `Skill "${scan.sameUrlName}" already bridges this URL - regenerate it with the fresh quote?`,
|
|
7874
|
-
default: true
|
|
7875
|
-
}
|
|
7876
|
-
]);
|
|
7877
|
-
if (!update) {
|
|
7878
|
-
fail("aborted");
|
|
7879
|
-
}
|
|
7880
|
-
}
|
|
7881
|
-
targetDir = scan.sameUrlDir;
|
|
7882
|
-
} else if (scan.dTagCollision !== void 0) {
|
|
7883
|
-
fail(
|
|
7884
|
-
`the generated name "${skillName}" collides with existing skill "${scan.dTagCollision}" (both map to d-tag "${candidateDTag}", which would replace its live discovery card) - pass a distinct --name`
|
|
7885
|
-
);
|
|
7886
|
-
} else if (existsSync(targetDir)) {
|
|
7887
|
-
fail(`skill directory ${targetDir} already exists - pass a distinct --name`);
|
|
7888
|
-
}
|
|
7889
|
-
let queryParam = options.queryParam;
|
|
7890
|
-
if (method === "GET" && queryParam === void 0 && !options.yes) {
|
|
7891
|
-
const { param } = await inquirer.prompt([
|
|
7892
|
-
{
|
|
7893
|
-
type: "input",
|
|
7894
|
-
name: "param",
|
|
7895
|
-
message: "GET upstream: which query parameter should carry the buyer input? (empty = the skill takes no input)"
|
|
7896
|
-
}
|
|
7897
|
-
]);
|
|
7898
|
-
queryParam = param.trim() === "" ? void 0 : param.trim();
|
|
7899
|
-
}
|
|
7900
|
-
if (queryParam !== void 0 && parsedUrl.searchParams.has(queryParam)) {
|
|
7901
|
-
fail(`--query-param "${queryParam}" collides with a parameter already present in the URL`);
|
|
7902
|
-
}
|
|
7903
|
-
if (method === "POST" && queryParam !== void 0) {
|
|
7904
|
-
fail("--query-param only applies to GET upstreams (POST maps the input to the request body)");
|
|
7905
|
-
}
|
|
7906
|
-
const noInput = method === "GET" && queryParam === void 0;
|
|
7907
|
-
const feePercent = new Decimal(feeBps).div(100).toString();
|
|
7908
|
-
const marginPercent = new Decimal(marginBps).div(100).toString();
|
|
7909
|
-
console.log("");
|
|
7910
|
-
console.log(` ${chalk.bold(skillName)}`);
|
|
7911
|
-
console.log(` ${description}`);
|
|
7912
|
-
console.log("");
|
|
7913
|
-
console.log(
|
|
7914
|
-
` Upstream ${url} (${method}${queryParam !== void 0 ? `, input in ?${queryParam}=` : ""}${noInput ? ", no buyer input" : ""})`
|
|
7915
|
-
);
|
|
7916
|
-
console.log(` Quote ${formatAssetAmount(USDC_SOLANA_DEVNET, quote)}`);
|
|
7917
|
-
console.log(
|
|
7918
|
-
` Your price ${formatAssetAmount(USDC_SOLANA_DEVNET, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
|
|
7919
|
-
);
|
|
7920
|
-
console.log(` Net margin ${formatAssetAmount(USDC_SOLANA_DEVNET, netMargin)} per job`);
|
|
7921
|
-
console.log(
|
|
7922
|
-
` Float ${formatAssetAmount(USDC_SOLANA_DEVNET, balance)} in ${signer.address}`
|
|
7923
|
-
);
|
|
7924
|
-
if (balance < quote) {
|
|
7925
|
-
console.log(
|
|
7926
|
-
chalk.yellow(
|
|
7927
|
-
` ! Float below one upstream quote - fund the wallet with devnet USDC: ${CIRCLE_FAUCET_URL}`
|
|
7928
|
-
)
|
|
7929
|
-
);
|
|
7930
|
-
}
|
|
7931
|
-
console.log(` Skill dir ${targetDir}`);
|
|
7932
|
-
console.log("");
|
|
7933
|
-
if (!options.yes) {
|
|
7934
|
-
const { confirmed } = await inquirer.prompt([
|
|
7935
|
-
{ type: "confirm", name: "confirmed", message: "Write the skill?", default: true }
|
|
7936
|
-
]);
|
|
7937
|
-
if (!confirmed) {
|
|
7938
|
-
fail("aborted");
|
|
7939
|
-
}
|
|
8685
|
+
if (!solPayment?.address) {
|
|
8686
|
+
console.error("Solana address not configured for this agent.");
|
|
8687
|
+
process.exit(1);
|
|
7940
8688
|
}
|
|
7941
|
-
const
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
url,
|
|
7947
|
-
method,
|
|
7948
|
-
queryParam,
|
|
7949
|
-
quote,
|
|
7950
|
-
marginPercent,
|
|
7951
|
-
feePercent
|
|
7952
|
-
});
|
|
7953
|
-
const roundTrip = parseSkillMd(content);
|
|
7954
|
-
validateSkillFrontmatter(roundTrip.frontmatter, roundTrip.systemPrompt, {
|
|
7955
|
-
allowFreeSkills: false,
|
|
7956
|
-
allowX402Skills: true
|
|
7957
|
-
});
|
|
7958
|
-
await mkdir(targetDir, { recursive: true });
|
|
7959
|
-
await writeFile(join(targetDir, "SKILL.md"), content, "utf-8");
|
|
7960
|
-
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
8689
|
+
const rpcUrl = getRpcUrl(solPayment.network);
|
|
8690
|
+
const rpc = createSolanaRpc(rpcUrl);
|
|
8691
|
+
const walletAddress = address(solPayment.address);
|
|
8692
|
+
const { value: balance } = await rpc.getBalance(walletAddress).send();
|
|
8693
|
+
const usdcBalance = await fetchUsdcBalance(rpc, walletAddress);
|
|
7961
8694
|
console.log(`
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
}
|
|
7966
|
-
console.log(`
|
|
8695
|
+
Agent: ${name}`);
|
|
8696
|
+
console.log(` Network: ${solPayment.network}`);
|
|
8697
|
+
console.log(` Address: ${solPayment.address}`);
|
|
8698
|
+
console.log(` SOL balance: ${formatSol(Number(balance))} (${balance} lamports)`);
|
|
8699
|
+
console.log(` USDC balance: ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}
|
|
7967
8700
|
`);
|
|
7968
8701
|
}
|
|
7969
8702
|
function readPackageVersion() {
|
|
@@ -8039,6 +8772,14 @@ program.command("list").description("List all agents (project-local and home-glo
|
|
|
8039
8772
|
})
|
|
8040
8773
|
);
|
|
8041
8774
|
program.command("wallet [name]").description("Show wallet balance").action(safe(cmdWallet));
|
|
8775
|
+
program.command("delegate-key [name]").description("Generate, show, or rotate the agent's dedicated delegate signing key (spl-approve)").option("--show", "Print the existing delegate pubkey/address only").option(
|
|
8776
|
+
"--import",
|
|
8777
|
+
"Import an existing key (solana-keygen JSON path or base58) instead of generating"
|
|
8778
|
+
).option("--rotate", "Replace an existing delegate key (invalidates outstanding owner approvals)").option("--yes", "Skip confirmation prompts (requires an explicit agent argument)").action(
|
|
8779
|
+
safe(async (name, options) => {
|
|
8780
|
+
await cmdDelegateKey(name, options);
|
|
8781
|
+
})
|
|
8782
|
+
);
|
|
8042
8783
|
var identity = program.command("identity").description("Link, verify, and unlink external identities (GitHub, X, website)");
|
|
8043
8784
|
identity.command("link <platform> [agent]").description(
|
|
8044
8785
|
"Link a github|x|website identity: prints the NIP-39 proof template, verifies the proof live, writes elisym.yaml, and publishes the claim (kind 10011 / kind-0 nip05)"
|