@elisym/cli 0.26.1 → 0.27.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/README.md +26 -21
- package/dist/index.js +566 -183
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
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,
|
|
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';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, splAssetsForNetwork, formatSol, formatAssetAmount, signerFromSecretKeyBase58, resolveUsdcAsset, 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, readAgentPublic, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
7
7
|
import { isAddress, createSolanaRpc, address, signature } from '@solana/kit';
|
|
8
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
9
9
|
import YAML2 from 'yaml';
|
|
@@ -1064,8 +1064,18 @@ var init_llm = __esm({
|
|
|
1064
1064
|
var init_exports = {};
|
|
1065
1065
|
__export(init_exports, {
|
|
1066
1066
|
cmdInit: () => cmdInit,
|
|
1067
|
-
fetchModels: () => fetchModels3
|
|
1067
|
+
fetchModels: () => fetchModels3,
|
|
1068
|
+
parseNetworkOption: () => parseNetworkOption
|
|
1068
1069
|
});
|
|
1070
|
+
function parseNetworkOption(raw) {
|
|
1071
|
+
if (raw === void 0) {
|
|
1072
|
+
return void 0;
|
|
1073
|
+
}
|
|
1074
|
+
if (raw === "devnet" || raw === "mainnet") {
|
|
1075
|
+
return raw;
|
|
1076
|
+
}
|
|
1077
|
+
throw new Error(`--network must be "devnet" or "mainnet"; got "${raw}"`);
|
|
1078
|
+
}
|
|
1069
1079
|
function buildDefaultYaml() {
|
|
1070
1080
|
return ElisymYamlSchema.parse({
|
|
1071
1081
|
description: "An elisym AI agent",
|
|
@@ -1097,6 +1107,7 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
1097
1107
|
if (options.config && options.defaults) {
|
|
1098
1108
|
throw new Error("--config and --defaults are mutually exclusive; pick one.");
|
|
1099
1109
|
}
|
|
1110
|
+
const networkFlag = parseNetworkOption(options.network);
|
|
1100
1111
|
const skipConfirms = options.yes || options.defaults;
|
|
1101
1112
|
let template;
|
|
1102
1113
|
if (options.config) {
|
|
@@ -1105,9 +1116,20 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
1105
1116
|
template = ElisymYamlSchema.parse(YAML2.parse(raw) ?? {});
|
|
1106
1117
|
console.log(` Loaded template from ${configPath}
|
|
1107
1118
|
`);
|
|
1119
|
+
const templateSolana = template.payments.find((entry) => entry.chain === "solana");
|
|
1120
|
+
if (networkFlag !== void 0 && templateSolana && templateSolana.network !== networkFlag) {
|
|
1121
|
+
throw new Error(
|
|
1122
|
+
`--network ${networkFlag} conflicts with the template's payments[].network (${templateSolana.network}). Edit the template or drop the flag.`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1108
1125
|
} else if (options.defaults) {
|
|
1109
1126
|
template = buildDefaultYaml();
|
|
1110
1127
|
console.log(" Using default skeleton (no LLM, no payments). Edit later via `profile`.\n");
|
|
1128
|
+
if (networkFlag !== void 0) {
|
|
1129
|
+
console.warn(
|
|
1130
|
+
" ! --network has no effect with --defaults (no payments entry is created). The wallet network is chosen when you configure a wallet.\n"
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1111
1133
|
}
|
|
1112
1134
|
const agentName = await resolveAgentName2(nameArg, inquirer);
|
|
1113
1135
|
const target = pickTarget(options);
|
|
@@ -1166,7 +1188,7 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
1166
1188
|
if (template) {
|
|
1167
1189
|
yaml = template;
|
|
1168
1190
|
} else {
|
|
1169
|
-
const result = await promptYaml(inquirer);
|
|
1191
|
+
const result = await promptYaml(inquirer, networkFlag);
|
|
1170
1192
|
yaml = result.yaml;
|
|
1171
1193
|
promptedApiKey = result.apiKey;
|
|
1172
1194
|
}
|
|
@@ -1259,6 +1281,14 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
1259
1281
|
]);
|
|
1260
1282
|
}
|
|
1261
1283
|
}
|
|
1284
|
+
const mainnetBound = yaml.payments.some(
|
|
1285
|
+
(entry) => entry.chain === "solana" && entry.network === "mainnet"
|
|
1286
|
+
);
|
|
1287
|
+
if (mainnetBound && !passphrase) {
|
|
1288
|
+
console.warn(
|
|
1289
|
+
" ! No passphrase set: this MAINNET agent will store its secrets unencrypted on disk.\n Strongly consider one (--passphrase or ELISYM_PASSPHRASE) - its keys will guard real funds."
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1262
1292
|
const nostrSecretBytes = generateSecretKey();
|
|
1263
1293
|
const nostrPubkey = getPublicKey(nostrSecretBytes);
|
|
1264
1294
|
const nostrSecretHex = Buffer.from(nostrSecretBytes).toString("hex");
|
|
@@ -1284,7 +1314,7 @@ async function cmdInit(nameArg, options = {}) {
|
|
|
1284
1314
|
console.log(` Location: ${created.dir}`);
|
|
1285
1315
|
console.log(` Nostr: ${npub}`);
|
|
1286
1316
|
if (yaml.payments[0]?.address) {
|
|
1287
|
-
console.log(` Solana: ${yaml.payments[0].address}`);
|
|
1317
|
+
console.log(` Solana: ${yaml.payments[0].address} (${yaml.payments[0].network})`);
|
|
1288
1318
|
}
|
|
1289
1319
|
if (passphrase) {
|
|
1290
1320
|
console.log(" Secrets encrypted with your passphrase.");
|
|
@@ -1314,7 +1344,7 @@ async function resolveAgentName2(nameArg, inquirer) {
|
|
|
1314
1344
|
validateAgentName(inputName);
|
|
1315
1345
|
return inputName;
|
|
1316
1346
|
}
|
|
1317
|
-
async function promptYaml(inquirer) {
|
|
1347
|
+
async function promptYaml(inquirer, networkFlag) {
|
|
1318
1348
|
const { description } = await inquirer.prompt([
|
|
1319
1349
|
{
|
|
1320
1350
|
type: "input",
|
|
@@ -1361,10 +1391,34 @@ async function promptYaml(inquirer) {
|
|
|
1361
1391
|
}
|
|
1362
1392
|
}
|
|
1363
1393
|
]);
|
|
1394
|
+
let walletNetwork = "devnet";
|
|
1364
1395
|
if (solanaAddress) {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1396
|
+
if (networkFlag !== void 0) {
|
|
1397
|
+
walletNetwork = networkFlag;
|
|
1398
|
+
} else {
|
|
1399
|
+
const { network } = await inquirer.prompt([
|
|
1400
|
+
{
|
|
1401
|
+
type: "list",
|
|
1402
|
+
name: "network",
|
|
1403
|
+
message: "Solana network (fixed at init - to go mainnet later, create a new agent):",
|
|
1404
|
+
choices: [
|
|
1405
|
+
{ name: "devnet (test tokens - recommended to start)", value: "devnet" },
|
|
1406
|
+
{ name: "mainnet (REAL funds)", value: "mainnet" }
|
|
1407
|
+
],
|
|
1408
|
+
default: "devnet"
|
|
1409
|
+
}
|
|
1410
|
+
]);
|
|
1411
|
+
walletNetwork = network;
|
|
1412
|
+
}
|
|
1413
|
+
if (walletNetwork === "mainnet") {
|
|
1414
|
+
console.log(
|
|
1415
|
+
" ! MAINNET: this wallet handles REAL funds - every skill price is real money\n and there is no faucet. Fund it with real SOL (and USDC if skills price in it).\n The network is fixed at init: to go back to devnet, create a new agent."
|
|
1416
|
+
);
|
|
1417
|
+
} else {
|
|
1418
|
+
console.log(
|
|
1419
|
+
" The wallet receives every asset on Solana (SOL directly, USDC and other\n SPL tokens via their ATA). Each skill declares its own price and token\n in SKILL.md. Fund with SOL via `solana airdrop` or USDC via https://faucet.circle.com."
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1368
1422
|
}
|
|
1369
1423
|
const { llmProvider } = await inquirer.prompt([
|
|
1370
1424
|
{
|
|
@@ -1389,7 +1443,7 @@ async function promptYaml(inquirer) {
|
|
|
1389
1443
|
picture: picture || void 0,
|
|
1390
1444
|
banner: banner || void 0,
|
|
1391
1445
|
relays: [...RELAYS],
|
|
1392
|
-
payments: solanaAddress ? [{ chain: "solana", network:
|
|
1446
|
+
payments: solanaAddress ? [{ chain: "solana", network: walletNetwork, address: solanaAddress }] : [],
|
|
1393
1447
|
security: {}
|
|
1394
1448
|
};
|
|
1395
1449
|
if (llmProvider === "none") {
|
|
@@ -1450,15 +1504,24 @@ var WATCHDOG_PROBE_TIMEOUT_MS = 1e4;
|
|
|
1450
1504
|
var WATCHDOG_SELF_PING_INTERVAL_MS = 10 * 60 * 1e3;
|
|
1451
1505
|
var WATCHDOG_SELF_PING_TIMEOUT_MS = 15e3;
|
|
1452
1506
|
var WATCHDOG_SLEEP_DETECT_MULTIPLIER = 2;
|
|
1453
|
-
function getRpcUrl(
|
|
1507
|
+
function getRpcUrl(network) {
|
|
1454
1508
|
const envUrl = process.env.SOLANA_RPC_URL;
|
|
1455
1509
|
if (envUrl) {
|
|
1456
1510
|
return envUrl;
|
|
1457
1511
|
}
|
|
1458
|
-
return "https://api.devnet.solana.com";
|
|
1512
|
+
return network === "mainnet" ? "https://api.mainnet-beta.solana.com" : "https://api.devnet.solana.com";
|
|
1459
1513
|
}
|
|
1460
|
-
async function
|
|
1461
|
-
|
|
1514
|
+
async function solanaLineFor(agent) {
|
|
1515
|
+
try {
|
|
1516
|
+
const { yaml } = await readAgentPublic(agent);
|
|
1517
|
+
const solana = yaml.payments.find((entry) => entry.chain === "solana");
|
|
1518
|
+
return solana ? ` | Solana: ${solana.address} (${solana.network})` : "";
|
|
1519
|
+
} catch {
|
|
1520
|
+
return "";
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async function fetchSplBalance(rpc, owner, asset) {
|
|
1524
|
+
const mint = asset.mint;
|
|
1462
1525
|
if (!mint) {
|
|
1463
1526
|
return 0n;
|
|
1464
1527
|
}
|
|
@@ -1472,21 +1535,39 @@ async function fetchUsdcBalance(rpc, owner) {
|
|
|
1472
1535
|
for (const entry of response.value) {
|
|
1473
1536
|
const parsed = entry.account.data;
|
|
1474
1537
|
const raw = parsed?.parsed?.info?.tokenAmount?.amount;
|
|
1475
|
-
if (typeof raw
|
|
1476
|
-
|
|
1538
|
+
if (typeof raw !== "string") {
|
|
1539
|
+
return null;
|
|
1477
1540
|
}
|
|
1541
|
+
total += BigInt(raw);
|
|
1478
1542
|
}
|
|
1479
1543
|
return total;
|
|
1480
1544
|
} catch {
|
|
1481
|
-
return
|
|
1545
|
+
return null;
|
|
1482
1546
|
}
|
|
1483
1547
|
}
|
|
1548
|
+
function formatSplBalanceValue(asset, balance) {
|
|
1549
|
+
return balance === void 0 || balance === null ? "unavailable (balance read failed)" : formatAssetAmount(asset, balance);
|
|
1550
|
+
}
|
|
1551
|
+
async function fetchUsdcBalance(rpc, owner, network) {
|
|
1552
|
+
return fetchSplBalance(rpc, owner, resolveUsdcAsset(network));
|
|
1553
|
+
}
|
|
1484
1554
|
|
|
1485
1555
|
// src/x402/constants.ts
|
|
1486
1556
|
var X402_SOLANA_DEVNET_CAIP2 = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
1487
1557
|
var X402_SOLANA_DEVNET_V1 = "solana-devnet";
|
|
1558
|
+
var X402_SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
1559
|
+
var X402_SOLANA_MAINNET_V1 = "solana";
|
|
1560
|
+
function x402SolanaNetworkIds(network) {
|
|
1561
|
+
return network === "mainnet" ? { caip2: X402_SOLANA_MAINNET_CAIP2, v1: X402_SOLANA_MAINNET_V1 } : { caip2: X402_SOLANA_DEVNET_CAIP2, v1: X402_SOLANA_DEVNET_V1 };
|
|
1562
|
+
}
|
|
1488
1563
|
var X402_GET_INPUT_MAX_ENCODED_BYTES = 2048;
|
|
1489
1564
|
var X402_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
1565
|
+
var X402_ERROR_EXCERPT_CHARS = 400;
|
|
1566
|
+
var X402_MAX_ERROR_BODY_BYTES = 8 * 1024;
|
|
1567
|
+
var X402_ERROR_BODY_READ_MS = 5e3;
|
|
1568
|
+
var X402_MAX_ERROR_BODY_READS = 2048;
|
|
1569
|
+
var X402_MAX_CHALLENGE_HEADER_CHARS = 64 * 1024;
|
|
1570
|
+
var X402_MASKED_INPUT_MIN_CHARS = 8;
|
|
1490
1571
|
var X402_MAX_CHALLENGE_BYTES = 256 * 1024;
|
|
1491
1572
|
var X402_PROBE_TTL_MS = 3e4;
|
|
1492
1573
|
var X402_MAX_PAID_ATTEMPTS = 2;
|
|
@@ -1494,8 +1575,6 @@ var X402_MAX_PAYMENT_SIGNATURES = 2 * X402_MAX_PAID_ATTEMPTS;
|
|
|
1494
1575
|
var X402_FREE_RETRY_DELAYS_MS = [1e3, 3e3];
|
|
1495
1576
|
var X402_REFUNDED_RETRIES = 1;
|
|
1496
1577
|
var X402_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
1497
|
-
|
|
1498
|
-
// src/x402/matcher.ts
|
|
1499
1578
|
function requirementAmount(requirement) {
|
|
1500
1579
|
const v1Amount = requirement.maxAmountRequired;
|
|
1501
1580
|
const raw = requirement.amount ?? v1Amount;
|
|
@@ -1509,10 +1588,12 @@ function isAcceptableRequirement(requirement, rule) {
|
|
|
1509
1588
|
return false;
|
|
1510
1589
|
}
|
|
1511
1590
|
const network = requirement.network;
|
|
1512
|
-
|
|
1591
|
+
const acceptedIds = x402SolanaNetworkIds(rule.network);
|
|
1592
|
+
if (network !== acceptedIds.caip2 && network !== acceptedIds.v1) {
|
|
1513
1593
|
return false;
|
|
1514
1594
|
}
|
|
1515
|
-
|
|
1595
|
+
const usdcMint = resolveUsdcAsset(rule.network).mint;
|
|
1596
|
+
if (usdcMint === void 0 || requirement.asset !== usdcMint) {
|
|
1516
1597
|
return false;
|
|
1517
1598
|
}
|
|
1518
1599
|
const amount = requirementAmount(requirement);
|
|
@@ -1697,7 +1778,6 @@ async function probePaymentRequiredCached(url, method, ttlMs) {
|
|
|
1697
1778
|
// src/commands/x402-add.ts
|
|
1698
1779
|
var BPS_DENOMINATOR = 1e4;
|
|
1699
1780
|
var DEFAULT_MARGIN_BPS = 1e3;
|
|
1700
|
-
var SOLANA_MAINNET_CAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
1701
1781
|
var CIRCLE_FAUCET_URL = "https://faucet.circle.com";
|
|
1702
1782
|
function fail(message) {
|
|
1703
1783
|
console.error(`
|
|
@@ -1782,7 +1862,7 @@ function buildSkillMd(input) {
|
|
|
1782
1862
|
"Operator notes for this x402 bridge skill (the executor ignores this body):",
|
|
1783
1863
|
"",
|
|
1784
1864
|
`- Upstream: ${input.url} (${input.method})`,
|
|
1785
|
-
`- Upstream quote at generation time: ${formatAssetAmount(
|
|
1865
|
+
`- Upstream quote at generation time: ${formatAssetAmount(resolveUsdcAsset(input.network), input.quote)} (= x402_max_upstream, the signing ceiling)`,
|
|
1786
1866
|
`- Price: ${input.priceDisplay} USDC (margin ${input.marginPercent}%, protocol fee ${input.feePercent}% at generation time)`,
|
|
1787
1867
|
"- To reprice after an upstream change, re-run:",
|
|
1788
1868
|
"",
|
|
@@ -1834,15 +1914,17 @@ function scanExistingSkills(skillsDir, candidateDTag, url) {
|
|
|
1834
1914
|
}
|
|
1835
1915
|
return scan;
|
|
1836
1916
|
}
|
|
1837
|
-
function describeUnacceptableAccepts(probe) {
|
|
1917
|
+
function describeUnacceptableAccepts(probe, agentNetwork) {
|
|
1838
1918
|
const networks = [...new Set(probe.accepts.map((requirement) => String(requirement.network)))];
|
|
1839
1919
|
if (networks.length > 0 && networks.every((network) => network.startsWith("eip155:"))) {
|
|
1840
1920
|
return `the service accepts only EVM networks (${networks.join(", ")}) - not supported yet`;
|
|
1841
1921
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1922
|
+
const otherNetwork = agentNetwork === "mainnet" ? "devnet" : "mainnet";
|
|
1923
|
+
const otherIds = x402SolanaNetworkIds(otherNetwork);
|
|
1924
|
+
if (networks.some((network) => network === otherIds.caip2 || network === otherIds.v1)) {
|
|
1925
|
+
return `the service settles on Solana ${otherNetwork} but this agent is bound to ${agentNetwork} (one agent = one network) - bridge it from a ${otherNetwork} agent instead` + (otherNetwork === "mainnet" ? " (create one: npx @elisym/cli init --network mainnet)" : "");
|
|
1844
1926
|
}
|
|
1845
|
-
return `no exact-scheme
|
|
1927
|
+
return `no exact-scheme ${agentNetwork}-USDC requirement found (service accepts: ${networks.join(", ") || "nothing parseable"})`;
|
|
1846
1928
|
}
|
|
1847
1929
|
async function cmdX402Add(url, agentName, options) {
|
|
1848
1930
|
const cwd = process.cwd();
|
|
@@ -1886,6 +1968,33 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
1886
1968
|
}
|
|
1887
1969
|
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
1888
1970
|
const loaded = await loadAgent(agentName, cwd, passphrase);
|
|
1971
|
+
const existingSolPayment = loaded.yaml.payments.find((entry) => entry.chain === "solana");
|
|
1972
|
+
let agentNetwork;
|
|
1973
|
+
if (existingSolPayment !== void 0) {
|
|
1974
|
+
agentNetwork = existingSolPayment.network;
|
|
1975
|
+
} else if (options.yes) {
|
|
1976
|
+
agentNetwork = "devnet";
|
|
1977
|
+
} else {
|
|
1978
|
+
const { network } = await inquirer.prompt([
|
|
1979
|
+
{
|
|
1980
|
+
type: "list",
|
|
1981
|
+
name: "network",
|
|
1982
|
+
message: "This agent has no Solana wallet entry yet. Which network should it operate on (fixed once set)?",
|
|
1983
|
+
choices: [
|
|
1984
|
+
{ name: "devnet (test tokens - recommended to start)", value: "devnet" },
|
|
1985
|
+
{ name: "mainnet (REAL funds)", value: "mainnet" }
|
|
1986
|
+
],
|
|
1987
|
+
default: "devnet"
|
|
1988
|
+
}
|
|
1989
|
+
]);
|
|
1990
|
+
agentNetwork = network;
|
|
1991
|
+
if (agentNetwork === "mainnet") {
|
|
1992
|
+
console.log(
|
|
1993
|
+
" ! MAINNET: the bridge float and its upstream payments are REAL funds.\n The network is fixed once written - to switch, create a new agent."
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
const usdcAsset = resolveUsdcAsset(agentNetwork);
|
|
1889
1998
|
console.log(`
|
|
1890
1999
|
Probing ${url} (${method}, unpaid)...`);
|
|
1891
2000
|
let probe;
|
|
@@ -1898,10 +2007,13 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
1898
2007
|
const message = error instanceof Error ? error.message : String(error);
|
|
1899
2008
|
fail(`upstream unreachable: ${message}`);
|
|
1900
2009
|
}
|
|
1901
|
-
const anyCeiling = {
|
|
2010
|
+
const anyCeiling = {
|
|
2011
|
+
maxUpstreamSubunits: BigInt(Number.MAX_SAFE_INTEGER),
|
|
2012
|
+
network: agentNetwork
|
|
2013
|
+
};
|
|
1902
2014
|
const requirement = selectAcceptableRequirement(probe.accepts, anyCeiling);
|
|
1903
2015
|
if (requirement === void 0) {
|
|
1904
|
-
fail(describeUnacceptableAccepts(probe));
|
|
2016
|
+
fail(describeUnacceptableAccepts(probe, agentNetwork));
|
|
1905
2017
|
}
|
|
1906
2018
|
const quote = requirementAmount(requirement);
|
|
1907
2019
|
if (quote === null || quote <= 0n) {
|
|
@@ -1963,6 +2075,11 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
1963
2075
|
);
|
|
1964
2076
|
}
|
|
1965
2077
|
}
|
|
2078
|
+
if (agentNetwork === "mainnet" && !passphrase) {
|
|
2079
|
+
console.warn(
|
|
2080
|
+
" ! No passphrase set: this MAINNET wallet key will be stored unencrypted on disk.\n Strongly consider one (ELISYM_PASSPHRASE) - the key will guard real funds."
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
1966
2083
|
await writeSecrets(
|
|
1967
2084
|
loaded.dir,
|
|
1968
2085
|
{ ...loaded.secrets, solana_secret_key: solanaSecretKey },
|
|
@@ -1971,33 +2088,35 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
1971
2088
|
console.log(" Wallet key saved to .secrets.json");
|
|
1972
2089
|
}
|
|
1973
2090
|
const signer = await signerFromSecretKeyBase58(solanaSecretKey);
|
|
1974
|
-
|
|
1975
|
-
if (solPayment !== void 0 && solPayment.address !== signer.address) {
|
|
2091
|
+
if (existingSolPayment !== void 0 && existingSolPayment.address !== signer.address) {
|
|
1976
2092
|
fail(
|
|
1977
|
-
`wallet invariant violated: elisym.yaml payments[].address (${
|
|
2093
|
+
`wallet invariant violated: elisym.yaml payments[].address (${existingSolPayment.address}) differs from the solana_secret_key address (${signer.address}).
|
|
1978
2094
|
Revenue must land in the wallet the bridge spends from, or the float never refills.
|
|
1979
2095
|
Fix: set payments[].address to the key address, or import the matching key.`
|
|
1980
2096
|
);
|
|
1981
2097
|
}
|
|
1982
|
-
if (
|
|
2098
|
+
if (existingSolPayment === void 0) {
|
|
1983
2099
|
console.log(
|
|
1984
|
-
` elisym.yaml has no Solana payments entry; adding address ${signer.address} (
|
|
2100
|
+
` elisym.yaml has no Solana payments entry; adding address ${signer.address} (${agentNetwork}).`
|
|
1985
2101
|
);
|
|
1986
2102
|
await writeYaml(loaded.dir, {
|
|
1987
2103
|
...loaded.yaml,
|
|
1988
2104
|
payments: [
|
|
1989
2105
|
...loaded.yaml.payments,
|
|
1990
|
-
{ chain: "solana", network:
|
|
2106
|
+
{ chain: "solana", network: agentNetwork, address: signer.address }
|
|
1991
2107
|
]
|
|
1992
2108
|
});
|
|
1993
2109
|
}
|
|
1994
|
-
const rpc = createSolanaRpc(getRpcUrl());
|
|
1995
|
-
const balance = await fetchUsdcBalance(rpc, address(signer.address));
|
|
2110
|
+
const rpc = createSolanaRpc(getRpcUrl(agentNetwork));
|
|
2111
|
+
const balance = await fetchUsdcBalance(rpc, address(signer.address), agentNetwork);
|
|
1996
2112
|
let feeBps;
|
|
1997
2113
|
try {
|
|
1998
|
-
const protocolConfig = await getProtocolConfig(
|
|
1999
|
-
|
|
2000
|
-
|
|
2114
|
+
const protocolConfig = await getProtocolConfig(
|
|
2115
|
+
rpc,
|
|
2116
|
+
getProtocolProgramId(agentNetwork),
|
|
2117
|
+
agentNetwork,
|
|
2118
|
+
{ forceRefresh: true }
|
|
2119
|
+
);
|
|
2001
2120
|
feeBps = protocolConfig.feeBps;
|
|
2002
2121
|
} catch (error) {
|
|
2003
2122
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2009,7 +2128,7 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
2009
2128
|
);
|
|
2010
2129
|
}
|
|
2011
2130
|
const priceSubunits = computeBridgePriceSubunits(quote, marginBps, feeBps);
|
|
2012
|
-
const priceDisplay = new Decimal(priceSubunits.toString()).div(new Decimal(10).pow(
|
|
2131
|
+
const priceDisplay = new Decimal(priceSubunits.toString()).div(new Decimal(10).pow(usdcAsset.decimals)).toString();
|
|
2013
2132
|
const protocolFee = calculateProtocolFee(Number(priceSubunits), feeBps);
|
|
2014
2133
|
const netMargin = priceSubunits - BigInt(protocolFee) - quote;
|
|
2015
2134
|
const fallbackName = slugFromUrl(parsedUrl);
|
|
@@ -2078,18 +2197,17 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
2078
2197
|
console.log(
|
|
2079
2198
|
` Upstream ${url} (${method}${queryParam !== void 0 ? `, input in ?${queryParam}=` : ""}${noInput ? ", no buyer input" : ""})`
|
|
2080
2199
|
);
|
|
2081
|
-
console.log(`
|
|
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`);
|
|
2200
|
+
console.log(` Network ${agentNetwork}`);
|
|
2201
|
+
console.log(` Quote ${formatAssetAmount(usdcAsset, quote)}`);
|
|
2086
2202
|
console.log(
|
|
2087
|
-
`
|
|
2203
|
+
` Your price ${formatAssetAmount(usdcAsset, priceSubunits)} (margin ${marginPercent}% + protocol fee ${feePercent}%)`
|
|
2088
2204
|
);
|
|
2089
|
-
|
|
2205
|
+
console.log(` Net margin ${formatAssetAmount(usdcAsset, netMargin)} per job`);
|
|
2206
|
+
console.log(` Float ${formatSplBalanceValue(usdcAsset, balance)} in ${signer.address}`);
|
|
2207
|
+
if (balance !== null && balance < quote) {
|
|
2090
2208
|
console.log(
|
|
2091
2209
|
chalk.yellow(
|
|
2092
|
-
` ! Float below one upstream quote - fund the wallet with devnet USDC: ${CIRCLE_FAUCET_URL}`
|
|
2210
|
+
agentNetwork === "mainnet" ? " ! Float below one upstream quote - fund the wallet with USDC (mainnet, REAL funds)" : ` ! Float below one upstream quote - fund the wallet with devnet USDC: ${CIRCLE_FAUCET_URL}`
|
|
2093
2211
|
)
|
|
2094
2212
|
);
|
|
2095
2213
|
}
|
|
@@ -2113,10 +2231,12 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
2113
2231
|
queryParam,
|
|
2114
2232
|
quote,
|
|
2115
2233
|
marginPercent,
|
|
2116
|
-
feePercent
|
|
2234
|
+
feePercent,
|
|
2235
|
+
network: agentNetwork
|
|
2117
2236
|
});
|
|
2118
2237
|
const roundTrip = parseSkillMd(content);
|
|
2119
2238
|
validateSkillFrontmatter(roundTrip.frontmatter, roundTrip.systemPrompt, {
|
|
2239
|
+
network: agentNetwork,
|
|
2120
2240
|
allowFreeSkills: false,
|
|
2121
2241
|
allowX402Skills: true
|
|
2122
2242
|
});
|
|
@@ -2125,8 +2245,10 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
2125
2245
|
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
2126
2246
|
console.log(`
|
|
2127
2247
|
Wrote ${join(targetDir, "SKILL.md")}`);
|
|
2128
|
-
if (balance < quote) {
|
|
2129
|
-
console.log(
|
|
2248
|
+
if (balance !== null && balance < quote) {
|
|
2249
|
+
console.log(
|
|
2250
|
+
agentNetwork === "mainnet" ? " Next: fund the float with USDC on mainnet (REAL funds)." : ` Next: fund the float (devnet USDC): ${CIRCLE_FAUCET_URL}`
|
|
2251
|
+
);
|
|
2130
2252
|
}
|
|
2131
2253
|
console.log(` Start the agent: npx @elisym/cli start ${agentName}
|
|
2132
2254
|
`);
|
|
@@ -2162,7 +2284,7 @@ async function resolveAgentName(name, cwd, nonInteractive) {
|
|
|
2162
2284
|
}
|
|
2163
2285
|
async function printDelegateStatus(delegateSecretKey, network) {
|
|
2164
2286
|
const signer = await signerFromSecretKeyBase58(delegateSecretKey);
|
|
2165
|
-
const rpc = createSolanaRpc(getRpcUrl());
|
|
2287
|
+
const rpc = createSolanaRpc(getRpcUrl(network));
|
|
2166
2288
|
let gasLine = "";
|
|
2167
2289
|
try {
|
|
2168
2290
|
const { value: balance } = await rpc.getBalance(address(signer.address)).send();
|
|
@@ -2186,7 +2308,7 @@ async function cmdDelegateKey(name, options) {
|
|
|
2186
2308
|
const agentName = await resolveAgentName(name, cwd, options.yes === true);
|
|
2187
2309
|
const passphrase = process.env.ELISYM_PASSPHRASE;
|
|
2188
2310
|
const loaded = await loadAgent(agentName, cwd, passphrase);
|
|
2189
|
-
loaded.yaml.payments.find((entry) => entry.chain === "solana")?.network ?? "devnet";
|
|
2311
|
+
const network = loaded.yaml.payments.find((entry) => entry.chain === "solana")?.network ?? "devnet";
|
|
2190
2312
|
const existing = loaded.secrets.solana_delegate_secret_key;
|
|
2191
2313
|
if (options.show) {
|
|
2192
2314
|
if (!existing) {
|
|
@@ -2195,12 +2317,12 @@ async function cmdDelegateKey(name, options) {
|
|
|
2195
2317
|
);
|
|
2196
2318
|
process.exit(1);
|
|
2197
2319
|
}
|
|
2198
|
-
await printDelegateStatus(existing);
|
|
2320
|
+
await printDelegateStatus(existing, network);
|
|
2199
2321
|
return;
|
|
2200
2322
|
}
|
|
2201
2323
|
if (existing && !options.rotate) {
|
|
2202
2324
|
console.log(` Agent "${agentName}" already has a delegate key.`);
|
|
2203
|
-
await printDelegateStatus(existing);
|
|
2325
|
+
await printDelegateStatus(existing, network);
|
|
2204
2326
|
console.log(" Use --rotate to replace it (this invalidates outstanding owner approvals).");
|
|
2205
2327
|
return;
|
|
2206
2328
|
}
|
|
@@ -2257,6 +2379,11 @@ async function cmdDelegateKey(name, options) {
|
|
|
2257
2379
|
process.exit(1);
|
|
2258
2380
|
}
|
|
2259
2381
|
}
|
|
2382
|
+
if (network === "mainnet" && !passphrase) {
|
|
2383
|
+
console.warn(
|
|
2384
|
+
" ! No passphrase set: this MAINNET delegate key will be stored unencrypted on disk.\n Strongly consider one (ELISYM_PASSPHRASE) - the key spends owner-approved real funds."
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2260
2387
|
await writeSecrets(
|
|
2261
2388
|
loaded.dir,
|
|
2262
2389
|
{ ...loaded.secrets, solana_delegate_secret_key: delegateSecretKey },
|
|
@@ -2265,7 +2392,7 @@ async function cmdDelegateKey(name, options) {
|
|
|
2265
2392
|
console.log(
|
|
2266
2393
|
existing ? "\n Delegate key rotated and saved." : "\n Delegate key generated and saved."
|
|
2267
2394
|
);
|
|
2268
|
-
await printDelegateStatus(delegateSecretKey);
|
|
2395
|
+
await printDelegateStatus(delegateSecretKey, network);
|
|
2269
2396
|
}
|
|
2270
2397
|
var IDENTITY_PLATFORMS = ["github", "x", "website"];
|
|
2271
2398
|
var X_PROOF_URL_HOSTS = /* @__PURE__ */ new Set([
|
|
@@ -2899,12 +3026,12 @@ async function cmdProfile(name) {
|
|
|
2899
3026
|
console.log(" Profile updated.\n");
|
|
2900
3027
|
}
|
|
2901
3028
|
if (section === "wallet") {
|
|
2902
|
-
const current = loaded.yaml.payments
|
|
3029
|
+
const current = loaded.yaml.payments.find((entry) => entry.chain === "solana");
|
|
2903
3030
|
const answers = await inquirer.prompt([
|
|
2904
3031
|
{
|
|
2905
3032
|
type: "input",
|
|
2906
3033
|
name: "address",
|
|
2907
|
-
message: "Solana address (empty to
|
|
3034
|
+
message: current ? "Solana address:" : "Solana address (empty to skip):",
|
|
2908
3035
|
default: current?.address ?? "",
|
|
2909
3036
|
validate: (value) => {
|
|
2910
3037
|
if (!value) {
|
|
@@ -2914,9 +3041,40 @@ async function cmdProfile(name) {
|
|
|
2914
3041
|
}
|
|
2915
3042
|
}
|
|
2916
3043
|
]);
|
|
3044
|
+
if (!answers.address && current) {
|
|
3045
|
+
console.log(
|
|
3046
|
+
" ! The wallet entry cannot be removed: it pins the agent network, which is\n fixed at creation - to change networks, create a new agent. To stop\n charging, set every skill price to 0 instead. Wallet unchanged.\n"
|
|
3047
|
+
);
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
let walletNetwork = current?.network;
|
|
3051
|
+
if (answers.address && walletNetwork === void 0) {
|
|
3052
|
+
const { network } = await inquirer.prompt([
|
|
3053
|
+
{
|
|
3054
|
+
type: "list",
|
|
3055
|
+
name: "network",
|
|
3056
|
+
message: "Solana network (fixed once set - to switch later, create a new agent):",
|
|
3057
|
+
choices: [
|
|
3058
|
+
{ name: "devnet (test tokens - recommended to start)", value: "devnet" },
|
|
3059
|
+
{ name: "mainnet (REAL funds)", value: "mainnet" }
|
|
3060
|
+
],
|
|
3061
|
+
default: "devnet"
|
|
3062
|
+
}
|
|
3063
|
+
]);
|
|
3064
|
+
walletNetwork = network;
|
|
3065
|
+
if (walletNetwork === "mainnet") {
|
|
3066
|
+
console.log(
|
|
3067
|
+
" ! MAINNET: this wallet handles REAL funds - every skill price is real money\n and there is no faucet. Strongly consider encrypting secrets with a passphrase."
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
const otherChainEntries = loaded.yaml.payments.filter((entry) => entry.chain !== "solana");
|
|
2917
3072
|
const nextYaml = {
|
|
2918
3073
|
...loaded.yaml,
|
|
2919
|
-
payments: answers.address
|
|
3074
|
+
payments: answers.address && walletNetwork !== void 0 ? [
|
|
3075
|
+
...otherChainEntries,
|
|
3076
|
+
{ chain: "solana", network: walletNetwork, address: answers.address }
|
|
3077
|
+
] : otherChainEntries
|
|
2920
3078
|
};
|
|
2921
3079
|
await writeYaml(loaded.dir, nextYaml);
|
|
2922
3080
|
loaded.yaml = nextYaml;
|
|
@@ -4748,14 +4906,11 @@ var AgentRuntime = class {
|
|
|
4748
4906
|
}
|
|
4749
4907
|
/** Fetch on-chain protocol config (fee, treasury). Always fetches fresh to avoid stale treasury. */
|
|
4750
4908
|
async fetchProtocolConfig() {
|
|
4751
|
-
|
|
4752
|
-
throw new Error(
|
|
4753
|
-
`Network "${this.config.network}" is not supported. Only "devnet" is available until the on-chain protocol program is deployed on mainnet.`
|
|
4754
|
-
);
|
|
4755
|
-
}
|
|
4756
|
-
const programId = getProtocolProgramId("devnet");
|
|
4909
|
+
const programId = getProtocolProgramId(this.config.network);
|
|
4757
4910
|
const rpc = createSolanaRpc(getRpcUrl(this.config.network));
|
|
4758
|
-
const config = await getProtocolConfig(rpc, programId, {
|
|
4911
|
+
const config = await getProtocolConfig(rpc, programId, this.config.network, {
|
|
4912
|
+
forceRefresh: true
|
|
4913
|
+
});
|
|
4759
4914
|
return { feeBps: config.feeBps, treasury: config.treasury };
|
|
4760
4915
|
}
|
|
4761
4916
|
async run() {
|
|
@@ -5243,12 +5398,6 @@ var AgentRuntime = class {
|
|
|
5243
5398
|
"this agent does not accept delegated payment."
|
|
5244
5399
|
);
|
|
5245
5400
|
}
|
|
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
5401
|
const network = this.config.network;
|
|
5253
5402
|
if (!skill || skill.delegation === void 0 || skill.priceSubunits <= 0) {
|
|
5254
5403
|
return reject(
|
|
@@ -5365,7 +5514,9 @@ var AgentRuntime = class {
|
|
|
5365
5514
|
// first delegated job does not dead-letter on a missing ATA.
|
|
5366
5515
|
ensureDestination: { owner: this.config.solanaAddress }
|
|
5367
5516
|
});
|
|
5368
|
-
const pull = await buildSignedPull(ctx.rpc, ctx.delegateSigner, instructions
|
|
5517
|
+
const pull = await buildSignedPull(ctx.rpc, ctx.delegateSigner, instructions, {
|
|
5518
|
+
network: ctx.network
|
|
5519
|
+
});
|
|
5369
5520
|
this.ledger.recordPullSignature(
|
|
5370
5521
|
job.jobId,
|
|
5371
5522
|
pull.signature,
|
|
@@ -5840,6 +5991,7 @@ var AgentRuntime = class {
|
|
|
5840
5991
|
this.config.solanaAddress,
|
|
5841
5992
|
jobPrice,
|
|
5842
5993
|
protocolConfig,
|
|
5994
|
+
this.config.network,
|
|
5843
5995
|
{ expirySecs: this.config.paymentTimeoutSecs, asset: jobAsset }
|
|
5844
5996
|
);
|
|
5845
5997
|
const requestJson = JSON.stringify(request);
|
|
@@ -6804,7 +6956,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
6804
6956
|
}
|
|
6805
6957
|
return skill;
|
|
6806
6958
|
}
|
|
6807
|
-
function loadSkillsFromDir(skillsDir, options
|
|
6959
|
+
function loadSkillsFromDir(skillsDir, options) {
|
|
6808
6960
|
const skills = [];
|
|
6809
6961
|
let entries;
|
|
6810
6962
|
try {
|
|
@@ -6826,10 +6978,16 @@ function loadSkillsFromDir(skillsDir, options = {}) {
|
|
|
6826
6978
|
const content = readFileSync(skillMdPath, "utf-8");
|
|
6827
6979
|
const { frontmatter, systemPrompt } = parseSkillMd(content);
|
|
6828
6980
|
const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
|
|
6981
|
+
network: options.network,
|
|
6829
6982
|
allowFreeSkills: true,
|
|
6830
6983
|
// The CLI runtime wires an x402 driver into the skill context, so
|
|
6831
6984
|
// x402 skills are executable here (SDK-only hosts reject them).
|
|
6832
|
-
allowX402Skills: true
|
|
6985
|
+
allowX402Skills: true,
|
|
6986
|
+
// Surface SDK load-time warnings (e.g. the devnet lsm->SOL pricing
|
|
6987
|
+
// fallback) in the start flow's ` !` console pattern.
|
|
6988
|
+
logger: {
|
|
6989
|
+
warn: (_obj, msg) => console.warn(` ! ${msg ?? ""}`)
|
|
6990
|
+
}
|
|
6833
6991
|
});
|
|
6834
6992
|
skills.push(buildCliSkill(parsed, entryPath, options.scriptEnv));
|
|
6835
6993
|
} catch (e) {
|
|
@@ -7368,6 +7526,15 @@ function isTextContentType(contentType) {
|
|
|
7368
7526
|
const mime = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
7369
7527
|
return mime.startsWith("text/") || mime === "application/json" || mime.endsWith("+json");
|
|
7370
7528
|
}
|
|
7529
|
+
function concatChunks(chunks, total) {
|
|
7530
|
+
const combined = new Uint8Array(total);
|
|
7531
|
+
let offset = 0;
|
|
7532
|
+
for (const chunk of chunks) {
|
|
7533
|
+
combined.set(chunk, offset);
|
|
7534
|
+
offset += chunk.byteLength;
|
|
7535
|
+
}
|
|
7536
|
+
return combined;
|
|
7537
|
+
}
|
|
7371
7538
|
async function readBodyCapped(response, maxBytes) {
|
|
7372
7539
|
const reader = response.body?.getReader();
|
|
7373
7540
|
if (reader === void 0) {
|
|
@@ -7392,13 +7559,43 @@ async function readBodyCapped(response, maxBytes) {
|
|
|
7392
7559
|
}
|
|
7393
7560
|
chunks.push(value);
|
|
7394
7561
|
}
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7562
|
+
return concatChunks(chunks, total);
|
|
7563
|
+
}
|
|
7564
|
+
async function readBodyPrefix(response, maxBytes, timeoutMs) {
|
|
7565
|
+
const reader = response.body?.getReader();
|
|
7566
|
+
if (reader === void 0) {
|
|
7567
|
+
return { text: "", truncated: false };
|
|
7400
7568
|
}
|
|
7401
|
-
|
|
7569
|
+
const chunks = [];
|
|
7570
|
+
let total = 0;
|
|
7571
|
+
let ended = false;
|
|
7572
|
+
let timedOut = false;
|
|
7573
|
+
const deadline = setTimeout(() => {
|
|
7574
|
+
timedOut = true;
|
|
7575
|
+
reader.cancel().catch(() => {
|
|
7576
|
+
});
|
|
7577
|
+
}, timeoutMs);
|
|
7578
|
+
try {
|
|
7579
|
+
for (let reads = 0; reads < X402_MAX_ERROR_BODY_READS && total < maxBytes; reads += 1) {
|
|
7580
|
+
const { done, value } = await reader.read();
|
|
7581
|
+
if (done) {
|
|
7582
|
+
ended = true;
|
|
7583
|
+
break;
|
|
7584
|
+
}
|
|
7585
|
+
const fitting = value.subarray(0, maxBytes - total);
|
|
7586
|
+
total += fitting.byteLength;
|
|
7587
|
+
chunks.push(fitting);
|
|
7588
|
+
}
|
|
7589
|
+
} catch {
|
|
7590
|
+
} finally {
|
|
7591
|
+
clearTimeout(deadline);
|
|
7592
|
+
}
|
|
7593
|
+
reader.cancel().catch(() => {
|
|
7594
|
+
});
|
|
7595
|
+
return {
|
|
7596
|
+
text: new TextDecoder().decode(concatChunks(chunks, total)),
|
|
7597
|
+
truncated: !ended || timedOut
|
|
7598
|
+
};
|
|
7402
7599
|
}
|
|
7403
7600
|
async function capChallengeResponse(response) {
|
|
7404
7601
|
if (response.status !== 402) {
|
|
@@ -7411,6 +7608,105 @@ async function capChallengeResponse(response) {
|
|
|
7411
7608
|
headers: response.headers
|
|
7412
7609
|
});
|
|
7413
7610
|
}
|
|
7611
|
+
var PAYMENT_IDENTIFIER_EXTENSION = "payment-identifier";
|
|
7612
|
+
function asRecord(value) {
|
|
7613
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
7614
|
+
}
|
|
7615
|
+
function extensionInfo(declaration) {
|
|
7616
|
+
if (declaration === null) {
|
|
7617
|
+
return {};
|
|
7618
|
+
}
|
|
7619
|
+
return Object.hasOwn(declaration, "info") ? asRecord(declaration.info) ?? {} : declaration;
|
|
7620
|
+
}
|
|
7621
|
+
function requiredPaymentIdentifier(response) {
|
|
7622
|
+
const header = response.headers.get("payment-required");
|
|
7623
|
+
if (header === null || header.length > X402_MAX_CHALLENGE_HEADER_CHARS) {
|
|
7624
|
+
return null;
|
|
7625
|
+
}
|
|
7626
|
+
try {
|
|
7627
|
+
const decoded = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
|
|
7628
|
+
const extensions = asRecord(asRecord(decoded)?.extensions);
|
|
7629
|
+
const declaration = asRecord(extensions?.[PAYMENT_IDENTIFIER_EXTENSION]);
|
|
7630
|
+
return extensionInfo(declaration).required === true ? declaration : null;
|
|
7631
|
+
} catch {
|
|
7632
|
+
return null;
|
|
7633
|
+
}
|
|
7634
|
+
}
|
|
7635
|
+
function withPaymentIdentifier(headerValue, declaration, jobId) {
|
|
7636
|
+
try {
|
|
7637
|
+
const record = asRecord(JSON.parse(Buffer.from(headerValue, "base64").toString("utf8")));
|
|
7638
|
+
if (record === null) {
|
|
7639
|
+
return headerValue;
|
|
7640
|
+
}
|
|
7641
|
+
const transaction = asRecord(record.payload)?.transaction;
|
|
7642
|
+
const signed = typeof transaction === "string" ? transaction : headerValue;
|
|
7643
|
+
const digest = createHash("sha256").update(`${jobId}:${signed}`).digest("hex");
|
|
7644
|
+
const id = `pay_${digest.slice(0, 32)}`;
|
|
7645
|
+
const extensions = asRecord(record.extensions) ?? {};
|
|
7646
|
+
const echoed = asRecord(extensions[PAYMENT_IDENTIFIER_EXTENSION]) ?? {};
|
|
7647
|
+
const info = { id, ...extensionInfo(declaration), ...extensionInfo(echoed) };
|
|
7648
|
+
record.extensions = {
|
|
7649
|
+
...extensions,
|
|
7650
|
+
// Mirror the shape the server declared. Its echo VALIDATOR would accept
|
|
7651
|
+
// either (`@x402/core` normalizes both sides through `getExtensionInfo`
|
|
7652
|
+
// first), but whatever reads the id back out afterwards is the server's
|
|
7653
|
+
// own code, and the one place it can be counted on to look is where it
|
|
7654
|
+
// declared the extension.
|
|
7655
|
+
[PAYMENT_IDENTIFIER_EXTENSION]: Object.hasOwn(declaration, "info") ? { ...echoed, info } : info
|
|
7656
|
+
};
|
|
7657
|
+
return Buffer.from(JSON.stringify(record), "utf8").toString("base64");
|
|
7658
|
+
} catch {
|
|
7659
|
+
return headerValue;
|
|
7660
|
+
}
|
|
7661
|
+
}
|
|
7662
|
+
var UNICODE_FORMAT_MARKS = /\p{Cf}/gu;
|
|
7663
|
+
function flattenForOperator(text) {
|
|
7664
|
+
return sanitizeForTerminal(text).replace(UNICODE_FORMAT_MARKS, "").replace(/\s+/g, " ").trim();
|
|
7665
|
+
}
|
|
7666
|
+
function printedPrefix(flattened) {
|
|
7667
|
+
return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : flattened.slice(0, X402_ERROR_EXCERPT_CHARS).replace(/[\ud800-\udbff]$/, "");
|
|
7668
|
+
}
|
|
7669
|
+
function clipForOperator(flattened) {
|
|
7670
|
+
return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : `${printedPrefix(flattened)}...`;
|
|
7671
|
+
}
|
|
7672
|
+
function quoteUpstream(text) {
|
|
7673
|
+
return clipForOperator(flattenForOperator(text));
|
|
7674
|
+
}
|
|
7675
|
+
function quoteUpstreamWithInput(text, sentInput) {
|
|
7676
|
+
return clipForOperator(maskCustomerInput(flattenForOperator(text), inputForms(sentInput)));
|
|
7677
|
+
}
|
|
7678
|
+
function wireForm(sentInput) {
|
|
7679
|
+
return new TextDecoder().decode(new TextEncoder().encode(sentInput));
|
|
7680
|
+
}
|
|
7681
|
+
function jsonEscapedForm(sentInput) {
|
|
7682
|
+
return JSON.stringify(sentInput).slice(1, -1);
|
|
7683
|
+
}
|
|
7684
|
+
function urlEncodedForms(sentInput) {
|
|
7685
|
+
const forms = [new URLSearchParams({ q: sentInput }).toString().slice("q=".length)];
|
|
7686
|
+
try {
|
|
7687
|
+
forms.push(encodeURIComponent(sentInput));
|
|
7688
|
+
} catch {
|
|
7689
|
+
}
|
|
7690
|
+
return forms;
|
|
7691
|
+
}
|
|
7692
|
+
function endsInsideInput(text, forms) {
|
|
7693
|
+
const tail = text.replace(/\ufffd+$/, "").slice(-X402_MASKED_INPUT_MIN_CHARS);
|
|
7694
|
+
return tail.length >= X402_MASKED_INPUT_MIN_CHARS && forms.some((form) => form.includes(tail));
|
|
7695
|
+
}
|
|
7696
|
+
function inputForms(sentInput) {
|
|
7697
|
+
return [wireForm(sentInput), jsonEscapedForm(sentInput), ...urlEncodedForms(sentInput)].map(
|
|
7698
|
+
flattenForOperator
|
|
7699
|
+
);
|
|
7700
|
+
}
|
|
7701
|
+
function maskCustomerInput(text, forms) {
|
|
7702
|
+
let masked = text;
|
|
7703
|
+
for (const form of forms) {
|
|
7704
|
+
if (form.length >= X402_MASKED_INPUT_MIN_CHARS) {
|
|
7705
|
+
masked = masked.replaceAll(form, "[input]");
|
|
7706
|
+
}
|
|
7707
|
+
}
|
|
7708
|
+
return masked;
|
|
7709
|
+
}
|
|
7414
7710
|
function abortableDelay(ms, signal) {
|
|
7415
7711
|
return new Promise((resolve4) => {
|
|
7416
7712
|
if (signal?.aborted) {
|
|
@@ -7433,15 +7729,17 @@ var X402Driver = class {
|
|
|
7433
7729
|
this.options = options;
|
|
7434
7730
|
this.store = new X402JobStore(options.agentDir);
|
|
7435
7731
|
this.rpc = createSolanaRpc(options.rpcUrl);
|
|
7732
|
+
this.usdcAsset = resolveUsdcAsset(options.network);
|
|
7436
7733
|
this.store.sweepExpired().catch(() => {
|
|
7437
7734
|
});
|
|
7438
7735
|
}
|
|
7439
7736
|
store;
|
|
7440
7737
|
rpc;
|
|
7738
|
+
usdcAsset;
|
|
7441
7739
|
signerPromise = null;
|
|
7442
7740
|
repriceHintLogged = /* @__PURE__ */ new Set();
|
|
7443
7741
|
log(message) {
|
|
7444
|
-
(this.options.log ?? console.log)(`[x402] ${message}`);
|
|
7742
|
+
(this.options.log ?? console.log)(`[x402] ${flattenForOperator(message)}`);
|
|
7445
7743
|
}
|
|
7446
7744
|
getSigner() {
|
|
7447
7745
|
const secret = this.options.solanaSecretKeyBase58;
|
|
@@ -7498,13 +7796,16 @@ var X402Driver = class {
|
|
|
7498
7796
|
return;
|
|
7499
7797
|
}
|
|
7500
7798
|
const signer = await this.assertWalletInvariant();
|
|
7501
|
-
if (job.asset.mint !==
|
|
7799
|
+
if (job.asset.mint !== this.usdcAsset.mint || this.usdcAsset.mint === void 0) {
|
|
7502
7800
|
throw new X402PreflightError(
|
|
7503
|
-
`x402 skills must be priced in
|
|
7801
|
+
`x402 skills must be priced in ${this.options.network} USDC (skill "${job.skillName}" is priced in ${job.asset.symbol}); the margin check compares the elisym price to the upstream USDC quote - re-run \`npx @elisym/cli x402 add\``
|
|
7504
7802
|
);
|
|
7505
7803
|
}
|
|
7506
7804
|
this.assertInputRules(job, input);
|
|
7507
|
-
const rule = {
|
|
7805
|
+
const rule = {
|
|
7806
|
+
maxUpstreamSubunits: job.params.maxUpstreamSubunits,
|
|
7807
|
+
network: this.options.network
|
|
7808
|
+
};
|
|
7508
7809
|
let probe;
|
|
7509
7810
|
try {
|
|
7510
7811
|
probe = await probePaymentRequiredCached(
|
|
@@ -7514,10 +7815,10 @@ var X402Driver = class {
|
|
|
7514
7815
|
);
|
|
7515
7816
|
} catch (error) {
|
|
7516
7817
|
if (error instanceof X402ProbeError) {
|
|
7517
|
-
throw new X402PreflightError(`upstream probe failed: ${error.message}`);
|
|
7818
|
+
throw new X402PreflightError(`upstream probe failed: ${quoteUpstream(error.message)}`);
|
|
7518
7819
|
}
|
|
7519
7820
|
const message = error instanceof Error ? error.message : String(error);
|
|
7520
|
-
throw new X402PreflightError(`upstream unreachable: ${message}`);
|
|
7821
|
+
throw new X402PreflightError(`upstream unreachable: ${quoteUpstream(message)}`);
|
|
7521
7822
|
}
|
|
7522
7823
|
const quote = maxAcceptableQuote(probe.accepts, rule);
|
|
7523
7824
|
if (quote === null) {
|
|
@@ -7528,13 +7829,18 @@ var X402Driver = class {
|
|
|
7528
7829
|
if (quote < job.params.maxUpstreamSubunits && !this.repriceHintLogged.has(job.skillName)) {
|
|
7529
7830
|
this.repriceHintLogged.add(job.skillName);
|
|
7530
7831
|
this.log(
|
|
7531
|
-
`upstream for "${job.skillName}" now quotes ${formatAssetAmount(
|
|
7832
|
+
`upstream for "${job.skillName}" now quotes ${formatAssetAmount(this.usdcAsset, quote)} (your price was computed from ${formatAssetAmount(this.usdcAsset, job.params.maxUpstreamSubunits)}); re-run \`npx @elisym/cli x402 add\` to reprice`
|
|
7833
|
+
);
|
|
7834
|
+
}
|
|
7835
|
+
const balance = await fetchUsdcBalance(this.rpc, address(signer.address), this.options.network);
|
|
7836
|
+
if (balance === null) {
|
|
7837
|
+
throw new X402PreflightError(
|
|
7838
|
+
`could not read the bridge float balance for ${signer.address} - refusing to pay the upstream without verifying it; retry in a moment`
|
|
7532
7839
|
);
|
|
7533
7840
|
}
|
|
7534
|
-
const balance = await fetchUsdcBalance(this.rpc, address(signer.address));
|
|
7535
7841
|
if (balance < quote) {
|
|
7536
7842
|
throw new X402PreflightError(
|
|
7537
|
-
`bridge float insufficient: ${formatAssetAmount(
|
|
7843
|
+
`bridge float insufficient: ${formatAssetAmount(this.usdcAsset, balance)} USDC in ${signer.address}, upstream quotes ${formatAssetAmount(this.usdcAsset, quote)} - top up the wallet`
|
|
7538
7844
|
);
|
|
7539
7845
|
}
|
|
7540
7846
|
const feeBps = await this.options.getFeeBps();
|
|
@@ -7542,7 +7848,7 @@ var X402Driver = class {
|
|
|
7542
7848
|
const net = BigInt(job.priceSubunits - fee);
|
|
7543
7849
|
if (net < quote) {
|
|
7544
7850
|
throw new X402PreflightError(
|
|
7545
|
-
`negative margin: net revenue ${formatAssetAmount(
|
|
7851
|
+
`negative margin: net revenue ${formatAssetAmount(this.usdcAsset, net)} (price minus ${feeBps} bps protocol fee) is below the upstream quote ${formatAssetAmount(this.usdcAsset, quote)} - re-run \`npx @elisym/cli x402 add\` to reprice`
|
|
7546
7852
|
);
|
|
7547
7853
|
}
|
|
7548
7854
|
}
|
|
@@ -7558,6 +7864,7 @@ var X402Driver = class {
|
|
|
7558
7864
|
*/
|
|
7559
7865
|
buildInstrumentedFetch(jobId, attemptState) {
|
|
7560
7866
|
const store = this.store;
|
|
7867
|
+
let requiredIdentifier = null;
|
|
7561
7868
|
return async function instrumentedFetch(info, init) {
|
|
7562
7869
|
const request = new Request(info, init);
|
|
7563
7870
|
if (request.headers.has("PAYMENT-SIGNATURE") || request.headers.has("X-PAYMENT")) {
|
|
@@ -7572,19 +7879,30 @@ var X402Driver = class {
|
|
|
7572
7879
|
);
|
|
7573
7880
|
}
|
|
7574
7881
|
attemptState.paymentSent = true;
|
|
7882
|
+
const signature = request.headers.get("PAYMENT-SIGNATURE");
|
|
7883
|
+
if (requiredIdentifier !== null && signature !== null) {
|
|
7884
|
+
request.headers.set(
|
|
7885
|
+
"PAYMENT-SIGNATURE",
|
|
7886
|
+
withPaymentIdentifier(signature, requiredIdentifier, jobId)
|
|
7887
|
+
);
|
|
7888
|
+
}
|
|
7575
7889
|
}
|
|
7576
7890
|
const response = await globalThis.fetch(new Request(request, { redirect: "error" }));
|
|
7891
|
+
if (response.status === 402) {
|
|
7892
|
+
requiredIdentifier = requiredPaymentIdentifier(response);
|
|
7893
|
+
}
|
|
7577
7894
|
return capChallengeResponse(response);
|
|
7578
7895
|
};
|
|
7579
7896
|
}
|
|
7580
7897
|
buildClient(signer, rule) {
|
|
7581
7898
|
const scheme = new ExactSvmScheme(signer, { rpcUrl: this.options.rpcUrl });
|
|
7899
|
+
const networkIds = x402SolanaNetworkIds(this.options.network);
|
|
7582
7900
|
return x402Client.fromConfig({
|
|
7583
|
-
schemes: [{ network:
|
|
7901
|
+
schemes: [{ network: networkIds.caip2, client: scheme }],
|
|
7584
7902
|
policies: [buildRequirementsPolicy(rule)]
|
|
7585
|
-
}).registerV1(
|
|
7903
|
+
}).registerV1(networkIds.v1, scheme);
|
|
7586
7904
|
}
|
|
7587
|
-
classifyWrapperError(error) {
|
|
7905
|
+
classifyWrapperError(error, sentInput) {
|
|
7588
7906
|
if (error instanceof Error && error.name === "AbortError") {
|
|
7589
7907
|
return error;
|
|
7590
7908
|
}
|
|
@@ -7592,21 +7910,63 @@ var X402Driver = class {
|
|
|
7592
7910
|
return error;
|
|
7593
7911
|
}
|
|
7594
7912
|
const message = error instanceof Error ? error.message : String(error);
|
|
7913
|
+
const quoted = quoteUpstreamWithInput(message, sentInput);
|
|
7595
7914
|
if (message.includes("Failed to create payment payload")) {
|
|
7596
7915
|
return new X402PermanentError(
|
|
7597
|
-
`payment refused by policy (upstream likely repriced above x402_max_upstream): ${
|
|
7916
|
+
`payment refused by policy (upstream likely repriced above x402_max_upstream): ${quoted}`
|
|
7598
7917
|
);
|
|
7599
7918
|
}
|
|
7600
|
-
return new X402TransientError(`upstream request failed: ${
|
|
7919
|
+
return new X402TransientError(`upstream request failed: ${quoted}`);
|
|
7601
7920
|
}
|
|
7602
|
-
|
|
7921
|
+
/**
|
|
7922
|
+
* Read a short, safe excerpt of a failing upstream's body.
|
|
7923
|
+
*
|
|
7924
|
+
* A bare status code is not a diagnosis: a 4xx from an x402 upstream can
|
|
7925
|
+
* mean a rejected payload, a missing required extension, or an argument the
|
|
7926
|
+
* service did not like, and the operator cannot tell which without the
|
|
7927
|
+
* body's own words. The body is untrusted, so only a bounded prefix is read
|
|
7928
|
+
* at all, and it is stripped of anything that could forge a log line before
|
|
7929
|
+
* it reaches the operator's. Reading it never fails the caller - an
|
|
7930
|
+
* unreadable body simply yields no excerpt, which is exactly the
|
|
7931
|
+
* status-only message as before.
|
|
7932
|
+
*
|
|
7933
|
+
* `sentInput` is the customer's own job input, masked out of the quote by
|
|
7934
|
+
* `maskCustomerInput`: an upstream that echoes the request back would
|
|
7935
|
+
* otherwise put it in the operator's log, which the runtime keeps it out of
|
|
7936
|
+
* everywhere else.
|
|
7937
|
+
*/
|
|
7938
|
+
async errorExcerpt(response, sentInput) {
|
|
7939
|
+
try {
|
|
7940
|
+
const body = await readBodyPrefix(
|
|
7941
|
+
response,
|
|
7942
|
+
X402_MAX_ERROR_BODY_BYTES,
|
|
7943
|
+
this.options.errorBodyReadMs ?? X402_ERROR_BODY_READ_MS
|
|
7944
|
+
);
|
|
7945
|
+
const forms = inputForms(sentInput);
|
|
7946
|
+
const quoted = maskCustomerInput(flattenForOperator(body.text), forms);
|
|
7947
|
+
if (quoted.length === 0) {
|
|
7948
|
+
return "";
|
|
7949
|
+
}
|
|
7950
|
+
const shown = printedPrefix(quoted);
|
|
7951
|
+
if (body.truncated && endsInsideInput(shown, forms)) {
|
|
7952
|
+
return "";
|
|
7953
|
+
}
|
|
7954
|
+
return `: ${clipForOperator(quoted)}`;
|
|
7955
|
+
} catch {
|
|
7956
|
+
return "";
|
|
7957
|
+
}
|
|
7958
|
+
}
|
|
7959
|
+
classifyStatus(status, excerpt) {
|
|
7603
7960
|
if (status === 402) {
|
|
7604
|
-
return new X402TransientError(
|
|
7961
|
+
return new X402TransientError(
|
|
7962
|
+
`upstream still returned 402 after a payment attempt${excerpt}`,
|
|
7963
|
+
status
|
|
7964
|
+
);
|
|
7605
7965
|
}
|
|
7606
7966
|
if (status === 429 || status >= 500) {
|
|
7607
|
-
return new X402TransientError(`upstream returned ${status}`, status);
|
|
7967
|
+
return new X402TransientError(`upstream returned ${status}${excerpt}`, status);
|
|
7608
7968
|
}
|
|
7609
|
-
return new X402PermanentError(`upstream returned ${status}`);
|
|
7969
|
+
return new X402PermanentError(`upstream returned ${status}${excerpt}`);
|
|
7610
7970
|
}
|
|
7611
7971
|
async execute(job, input, signal) {
|
|
7612
7972
|
const cached = await this.store.getResult(input.jobId);
|
|
@@ -7637,7 +7997,10 @@ var X402Driver = class {
|
|
|
7637
7997
|
headers["content-type"] = jsonContentType(input.data) ? "application/json" : "text/plain";
|
|
7638
7998
|
}
|
|
7639
7999
|
const signer = await this.assertWalletInvariant();
|
|
7640
|
-
const rule = {
|
|
8000
|
+
const rule = {
|
|
8001
|
+
maxUpstreamSubunits: job.params.maxUpstreamSubunits,
|
|
8002
|
+
network: this.options.network
|
|
8003
|
+
};
|
|
7641
8004
|
const client = this.buildClient(signer, rule);
|
|
7642
8005
|
const jobTag = input.jobId.slice(0, 8);
|
|
7643
8006
|
let freeRetriesUsed = 0;
|
|
@@ -7650,7 +8013,7 @@ var X402Driver = class {
|
|
|
7650
8013
|
requestUrl,
|
|
7651
8014
|
headers,
|
|
7652
8015
|
body,
|
|
7653
|
-
input
|
|
8016
|
+
input,
|
|
7654
8017
|
client,
|
|
7655
8018
|
attemptState,
|
|
7656
8019
|
signal
|
|
@@ -7698,9 +8061,9 @@ var X402Driver = class {
|
|
|
7698
8061
|
}
|
|
7699
8062
|
}
|
|
7700
8063
|
/** One full payment-wrapper pass against the upstream; throws classified errors. */
|
|
7701
|
-
async attemptUpstream(job, requestUrl, headers, body,
|
|
8064
|
+
async attemptUpstream(job, requestUrl, headers, body, input, client, attemptState, signal) {
|
|
7702
8065
|
const fetchWithPayment = wrapFetchWithPayment(
|
|
7703
|
-
this.buildInstrumentedFetch(jobId, attemptState),
|
|
8066
|
+
this.buildInstrumentedFetch(input.jobId, attemptState),
|
|
7704
8067
|
client
|
|
7705
8068
|
);
|
|
7706
8069
|
let response;
|
|
@@ -7712,27 +8075,25 @@ var X402Driver = class {
|
|
|
7712
8075
|
signal
|
|
7713
8076
|
});
|
|
7714
8077
|
} catch (error) {
|
|
7715
|
-
throw this.classifyWrapperError(error);
|
|
8078
|
+
throw this.classifyWrapperError(error, input.data);
|
|
7716
8079
|
}
|
|
7717
8080
|
if (!response.ok) {
|
|
7718
|
-
|
|
7719
|
-
});
|
|
7720
|
-
throw this.classifyStatus(response.status);
|
|
8081
|
+
throw this.classifyStatus(response.status, await this.errorExcerpt(response, input.data));
|
|
7721
8082
|
}
|
|
7722
8083
|
let bytes;
|
|
7723
8084
|
try {
|
|
7724
8085
|
bytes = await readBodyCapped(response, X402_MAX_RESPONSE_BYTES);
|
|
7725
8086
|
} catch (error) {
|
|
7726
|
-
throw this.classifyWrapperError(error);
|
|
8087
|
+
throw this.classifyWrapperError(error, input.data);
|
|
7727
8088
|
}
|
|
7728
8089
|
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
7729
8090
|
if (isTextContentType(contentType)) {
|
|
7730
8091
|
const data = new TextDecoder().decode(bytes);
|
|
7731
|
-
await this.store.saveTextResult(jobId, data);
|
|
8092
|
+
await this.store.saveTextResult(input.jobId, data);
|
|
7732
8093
|
return { data };
|
|
7733
8094
|
}
|
|
7734
8095
|
const mime = contentType.split(";")[0]?.trim() ?? "application/octet-stream";
|
|
7735
|
-
const filePath = await this.store.saveFileResult(jobId, mime, bytes);
|
|
8096
|
+
const filePath = await this.store.saveFileResult(input.jobId, mime, bytes);
|
|
7736
8097
|
return { data: "", outputMime: mime, filePath };
|
|
7737
8098
|
}
|
|
7738
8099
|
};
|
|
@@ -7770,9 +8131,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
7770
8131
|
const rpcUrl = getRpcUrl(walletNetwork);
|
|
7771
8132
|
const rpc = createSolanaRpc(rpcUrl);
|
|
7772
8133
|
const walletAddress = address(solanaAddress);
|
|
7773
|
-
const
|
|
8134
|
+
const splAssets = splAssetsForNetwork(walletNetwork);
|
|
8135
|
+
const [{ value: balanceLamports }, ...splBalances] = await Promise.all([
|
|
7774
8136
|
rpc.getBalance(walletAddress).send(),
|
|
7775
|
-
|
|
8137
|
+
...splAssets.map((asset) => fetchSplBalance(rpc, walletAddress, asset))
|
|
7776
8138
|
]);
|
|
7777
8139
|
const balance = Number(balanceLamports);
|
|
7778
8140
|
console.log(" Wallet");
|
|
@@ -7782,9 +8144,16 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
7782
8144
|
console.log(` RPC ${stripRpcSecrets(process.env.SOLANA_RPC_URL)} (custom)`);
|
|
7783
8145
|
}
|
|
7784
8146
|
console.log(` SOL ${formatSol(balance)} (${balance} lamports)`);
|
|
7785
|
-
|
|
8147
|
+
for (const [index, asset] of splAssets.entries()) {
|
|
8148
|
+
const symbolColumn = asset.symbol.padEnd(8);
|
|
8149
|
+
console.log(` ${symbolColumn} ${formatSplBalanceValue(asset, splBalances[index])}`);
|
|
8150
|
+
}
|
|
7786
8151
|
if (balance === 0) {
|
|
7787
|
-
|
|
8152
|
+
if (walletNetwork === "mainnet") {
|
|
8153
|
+
console.log(" ! Wallet is empty. Fund it with real SOL to operate on mainnet.");
|
|
8154
|
+
} else {
|
|
8155
|
+
console.log(" ! Wallet is empty. Get devnet SOL: https://faucet.solana.com");
|
|
8156
|
+
}
|
|
7788
8157
|
}
|
|
7789
8158
|
console.log();
|
|
7790
8159
|
} catch (e) {
|
|
@@ -7793,10 +8162,15 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
7793
8162
|
`);
|
|
7794
8163
|
}
|
|
7795
8164
|
}
|
|
8165
|
+
if (walletNetwork === "mainnet") {
|
|
8166
|
+
console.log(
|
|
8167
|
+
" ! Mainnet: per-skill prices are REAL funds. Review each SKILL.md price - skills copied from a devnet agent keep their absolute amounts.\n"
|
|
8168
|
+
);
|
|
8169
|
+
}
|
|
7796
8170
|
const scriptEnv = buildScriptEnv(loaded.secrets);
|
|
7797
8171
|
const paths = agentPaths(loaded.dir);
|
|
7798
8172
|
const skillsDir = paths.skills;
|
|
7799
|
-
const allSkills = loadSkillsFromDir(skillsDir, { scriptEnv });
|
|
8173
|
+
const allSkills = loadSkillsFromDir(skillsDir, { network: walletNetwork, scriptEnv });
|
|
7800
8174
|
if (allSkills.length === 0) {
|
|
7801
8175
|
console.error(` ! No skills found in ${skillsDir}
|
|
7802
8176
|
`);
|
|
@@ -8009,9 +8383,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
8009
8383
|
}
|
|
8010
8384
|
}
|
|
8011
8385
|
for (const skill of x402Skills) {
|
|
8012
|
-
if (skill.asset.mint !==
|
|
8386
|
+
if (skill.asset.mint !== resolveUsdcAsset(walletNetwork).mint) {
|
|
8013
8387
|
console.warn(
|
|
8014
|
-
` ! x402 skill "${skill.name}" is priced in ${skill.asset.symbol}, not
|
|
8388
|
+
` ! x402 skill "${skill.name}" is priced in ${skill.asset.symbol}, not ${walletNetwork} USDC - every job will be refused at preflight. Re-run \`npx @elisym/cli x402 add\`.`
|
|
8015
8389
|
);
|
|
8016
8390
|
}
|
|
8017
8391
|
}
|
|
@@ -8021,24 +8395,27 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
8021
8395
|
" ! x402 skills will NOT be advertised and their jobs will be refused before payment."
|
|
8022
8396
|
);
|
|
8023
8397
|
console.warn(" ! Fix: re-run `npx @elisym/cli x402 add` or align elisym.yaml with the key.");
|
|
8398
|
+
} else {
|
|
8399
|
+
const x402RpcUrl = getRpcUrl(walletNetwork);
|
|
8400
|
+
async function fetchLiveFeeBps() {
|
|
8401
|
+
const config = await getProtocolConfig(
|
|
8402
|
+
createSolanaRpc(x402RpcUrl),
|
|
8403
|
+
getProtocolProgramId(walletNetwork),
|
|
8404
|
+
walletNetwork,
|
|
8405
|
+
{ forceRefresh: true }
|
|
8406
|
+
);
|
|
8407
|
+
return config.feeBps;
|
|
8408
|
+
}
|
|
8409
|
+
skillCtx.x402Driver = new X402Driver({
|
|
8410
|
+
agentDir: loaded.dir,
|
|
8411
|
+
paymentsAddress: solanaAddress,
|
|
8412
|
+
solanaSecretKeyBase58: loaded.secrets.solana_secret_key,
|
|
8413
|
+
rpcUrl: x402RpcUrl,
|
|
8414
|
+
network: walletNetwork,
|
|
8415
|
+
getFeeBps: fetchLiveFeeBps,
|
|
8416
|
+
log: (message) => console.log(` ${message}`)
|
|
8417
|
+
});
|
|
8024
8418
|
}
|
|
8025
|
-
const x402RpcUrl = getRpcUrl();
|
|
8026
|
-
async function fetchLiveFeeBps() {
|
|
8027
|
-
const config = await getProtocolConfig(
|
|
8028
|
-
createSolanaRpc(x402RpcUrl),
|
|
8029
|
-
getProtocolProgramId("devnet"),
|
|
8030
|
-
{ forceRefresh: true }
|
|
8031
|
-
);
|
|
8032
|
-
return config.feeBps;
|
|
8033
|
-
}
|
|
8034
|
-
skillCtx.x402Driver = new X402Driver({
|
|
8035
|
-
agentDir: loaded.dir,
|
|
8036
|
-
paymentsAddress: solanaAddress,
|
|
8037
|
-
solanaSecretKeyBase58: loaded.secrets.solana_secret_key,
|
|
8038
|
-
rpcUrl: x402RpcUrl,
|
|
8039
|
-
getFeeBps: fetchLiveFeeBps,
|
|
8040
|
-
log: (message) => console.log(` ${message}`)
|
|
8041
|
-
});
|
|
8042
8419
|
}
|
|
8043
8420
|
console.log(" Connecting to relays and publishing capabilities...");
|
|
8044
8421
|
const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
@@ -8216,11 +8593,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
8216
8593
|
const declaresDelegation = allSkills.some((skill) => skill.delegation !== void 0);
|
|
8217
8594
|
let delegatePubkey;
|
|
8218
8595
|
let delegateSigner;
|
|
8219
|
-
if (declaresDelegation
|
|
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) {
|
|
8596
|
+
if (declaresDelegation) {
|
|
8224
8597
|
const delegateSecret = loaded.secrets.solana_delegate_secret_key;
|
|
8225
8598
|
if (delegateSecret && delegateSecret.length > 0) {
|
|
8226
8599
|
try {
|
|
@@ -8231,7 +8604,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
8231
8604
|
const { value: delegateLamports } = await createSolanaRpc(getRpcUrl(walletNetwork)).getBalance(address(delegatePubkey)).send();
|
|
8232
8605
|
if (delegateLamports === 0n) {
|
|
8233
8606
|
console.warn(
|
|
8234
|
-
" ! Delegate key holds no SOL - delegated job pulls will fail on tx fees. Fund it: https://faucet.solana.com"
|
|
8607
|
+
" ! Delegate key holds no SOL - delegated job pulls will fail on tx fees. " + (walletNetwork === "mainnet" ? "Fund it with real SOL." : "Fund it: https://faucet.solana.com")
|
|
8235
8608
|
);
|
|
8236
8609
|
}
|
|
8237
8610
|
} catch {
|
|
@@ -8253,38 +8626,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
8253
8626
|
console.warn(` ! Run \`npx @elisym/cli delegate-key ${loaded.name}\` to enable it.`);
|
|
8254
8627
|
}
|
|
8255
8628
|
}
|
|
8256
|
-
|
|
8257
|
-
const isStatic = skill.mode === "static-file" || skill.mode === "static-script" || skill.noInput === true;
|
|
8258
|
-
return {
|
|
8259
|
-
name: skill.name,
|
|
8260
|
-
description: skill.description,
|
|
8261
|
-
capabilities: skill.capabilities,
|
|
8262
|
-
image: skill.image,
|
|
8263
|
-
...isStatic ? { static: true } : {},
|
|
8264
|
-
// File-exchange hints (dynamic-script only). `inputMime` flags a file input;
|
|
8265
|
-
// `inputText` tells the web whether to also show its text box for that file job.
|
|
8266
|
-
...skill.inputMime ? { inputMime: skill.inputMime } : {},
|
|
8267
|
-
...skill.inputText ? { inputText: skill.inputText } : {},
|
|
8268
|
-
...skill.outputMime ? { outputMime: skill.outputMime } : {},
|
|
8269
|
-
// Conversation-context flag: clients gate chat affordances (session-carrying
|
|
8270
|
-
// sends) on it. Strict true only - the read side coerces anything else away.
|
|
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 } } : {},
|
|
8276
|
-
payment: solanaAddress ? {
|
|
8277
|
-
chain: "solana",
|
|
8278
|
-
network: walletNetwork,
|
|
8279
|
-
address: solanaAddress,
|
|
8280
|
-
job_price: skill.priceSubunits,
|
|
8281
|
-
token: skill.asset.token,
|
|
8282
|
-
...skill.asset.mint ? { mint: skill.asset.mint } : {},
|
|
8283
|
-
decimals: skill.asset.decimals,
|
|
8284
|
-
symbol: skill.asset.symbol
|
|
8285
|
-
} : void 0
|
|
8286
|
-
};
|
|
8287
|
-
}
|
|
8629
|
+
const buildCard = (skill) => buildCapabilityCard(skill, { walletNetwork, solanaAddress, delegatePubkey });
|
|
8288
8630
|
function retiredModelKeyForSkill(skill) {
|
|
8289
8631
|
if (skill.resolvedTriple) {
|
|
8290
8632
|
return `${skill.resolvedTriple.provider}::${skill.resolvedTriple.model}`;
|
|
@@ -8391,7 +8733,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
8391
8733
|
if (nonceStore !== void 0) {
|
|
8392
8734
|
await ensureGitignoreHasDelegationNoncesEntry(dirname(loaded.dir));
|
|
8393
8735
|
}
|
|
8394
|
-
const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl());
|
|
8736
|
+
const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl(walletNetwork));
|
|
8395
8737
|
logger.debug(
|
|
8396
8738
|
{
|
|
8397
8739
|
event: "config_resolved",
|
|
@@ -8469,6 +8811,39 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
8469
8811
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
8470
8812
|
await runtime.run();
|
|
8471
8813
|
}
|
|
8814
|
+
function buildCapabilityCard(skill, inputs) {
|
|
8815
|
+
const { walletNetwork, solanaAddress, delegatePubkey } = inputs;
|
|
8816
|
+
const isStatic = skill.mode === "static-file" || skill.mode === "static-script" || skill.noInput === true;
|
|
8817
|
+
return {
|
|
8818
|
+
name: skill.name,
|
|
8819
|
+
description: skill.description,
|
|
8820
|
+
capabilities: skill.capabilities,
|
|
8821
|
+
image: skill.image,
|
|
8822
|
+
...isStatic ? { static: true } : {},
|
|
8823
|
+
// File-exchange hints (dynamic-script only). `inputMime` flags a file input;
|
|
8824
|
+
// `inputText` tells the web whether to also show its text box for that file job.
|
|
8825
|
+
...skill.inputMime ? { inputMime: skill.inputMime } : {},
|
|
8826
|
+
...skill.inputText ? { inputText: skill.inputText } : {},
|
|
8827
|
+
...skill.outputMime ? { outputMime: skill.outputMime } : {},
|
|
8828
|
+
// Conversation-context flag: clients gate chat affordances (session-carrying
|
|
8829
|
+
// sends) on it. Strict true only - the read side coerces anything else away.
|
|
8830
|
+
...skill.context === true ? { context: true } : {},
|
|
8831
|
+
// Delegation descriptor: only when the skill opts in AND a delegate key
|
|
8832
|
+
// exists (delegatePubkey resolved). The pubkey is injected here, never
|
|
8833
|
+
// taken from the SKILL.md - the operator declares only the cap/mechanism.
|
|
8834
|
+
...skill.delegation && delegatePubkey ? { delegation: { ...skill.delegation, delegate_pubkey: delegatePubkey } } : {},
|
|
8835
|
+
payment: solanaAddress ? {
|
|
8836
|
+
chain: "solana",
|
|
8837
|
+
network: walletNetwork,
|
|
8838
|
+
address: solanaAddress,
|
|
8839
|
+
job_price: skill.priceSubunits,
|
|
8840
|
+
token: skill.asset.token,
|
|
8841
|
+
...skill.asset.mint ? { mint: skill.asset.mint } : {},
|
|
8842
|
+
decimals: skill.asset.decimals,
|
|
8843
|
+
symbol: skill.asset.symbol
|
|
8844
|
+
} : void 0
|
|
8845
|
+
};
|
|
8846
|
+
}
|
|
8472
8847
|
function buildScriptEnv(secrets) {
|
|
8473
8848
|
const scriptEnv = { ...process.env };
|
|
8474
8849
|
delete scriptEnv.ELISYM_PASSPHRASE;
|
|
@@ -8689,15 +9064,20 @@ async function cmdWallet(name) {
|
|
|
8689
9064
|
const rpcUrl = getRpcUrl(solPayment.network);
|
|
8690
9065
|
const rpc = createSolanaRpc(rpcUrl);
|
|
8691
9066
|
const walletAddress = address(solPayment.address);
|
|
8692
|
-
const
|
|
8693
|
-
const
|
|
9067
|
+
const splAssets = splAssetsForNetwork(solPayment.network);
|
|
9068
|
+
const [{ value: balance }, ...splBalances] = await Promise.all([
|
|
9069
|
+
rpc.getBalance(walletAddress).send(),
|
|
9070
|
+
...splAssets.map((asset) => fetchSplBalance(rpc, walletAddress, asset))
|
|
9071
|
+
]);
|
|
8694
9072
|
console.log(`
|
|
8695
9073
|
Agent: ${name}`);
|
|
8696
9074
|
console.log(` Network: ${solPayment.network}`);
|
|
8697
9075
|
console.log(` Address: ${solPayment.address}`);
|
|
8698
9076
|
console.log(` SOL balance: ${formatSol(Number(balance))} (${balance} lamports)`);
|
|
8699
|
-
|
|
8700
|
-
`);
|
|
9077
|
+
for (const [index, asset] of splAssets.entries()) {
|
|
9078
|
+
console.log(` ${asset.symbol} balance: ${formatSplBalanceValue(asset, splBalances[index])}`);
|
|
9079
|
+
}
|
|
9080
|
+
console.log();
|
|
8701
9081
|
}
|
|
8702
9082
|
function readPackageVersion() {
|
|
8703
9083
|
try {
|
|
@@ -8727,6 +9107,9 @@ program.command("init [name]").description("Create a new agent").option("-c, --c
|
|
|
8727
9107
|
"--defaults",
|
|
8728
9108
|
"Skip every prompt and use the same defaults the wizard would have suggested (description, default relays, no payments, no LLM, no encryption). Mutually exclusive with --config."
|
|
8729
9109
|
).option("--local", "Create in project <project>/.elisym/<name>/ (default: ~/.elisym/<name>/)").option(
|
|
9110
|
+
"--network <network>",
|
|
9111
|
+
"Solana network for the wallet entry: devnet (default) or mainnet (REAL funds, no faucet). The network is fixed at init - to go mainnet later, create a new agent."
|
|
9112
|
+
).option(
|
|
8730
9113
|
"--passphrase <value>",
|
|
8731
9114
|
'Passphrase to encrypt secrets at rest. Empty string ("") skips encryption. Also reads ELISYM_PASSPHRASE env var. When neither is provided, prompts interactively.'
|
|
8732
9115
|
).option(
|
|
@@ -8756,16 +9139,16 @@ program.command("list").description("List all agents (project-local and home-glo
|
|
|
8756
9139
|
}
|
|
8757
9140
|
console.log("\nAgents:");
|
|
8758
9141
|
for (const agent of agents) {
|
|
9142
|
+
const solana = await solanaLineFor(agent);
|
|
9143
|
+
const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
|
|
8759
9144
|
try {
|
|
8760
9145
|
const loaded = await loadAgent(agent.name, cwd);
|
|
8761
9146
|
const identity2 = ElisymIdentity.fromHex(loaded.secrets.nostr_secret_key);
|
|
8762
9147
|
const npub = nip19.npubEncode(identity2.publicKey);
|
|
8763
|
-
|
|
8764
|
-
const shadow = agent.shadowsGlobal ? " [shadows global]" : "";
|
|
8765
|
-
console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solAddr}`);
|
|
9148
|
+
console.log(` ${agent.name} (${agent.source})${shadow} | ${npub}${solana}`);
|
|
8766
9149
|
} catch (e) {
|
|
8767
9150
|
const hint = /encrypted secrets/i.test(e?.message ?? "") ? " (encrypted)" : "";
|
|
8768
|
-
console.log(` ${agent.name} (${agent.source})${hint}`);
|
|
9151
|
+
console.log(` ${agent.name} (${agent.source})${shadow}${hint}${solana}`);
|
|
8769
9152
|
}
|
|
8770
9153
|
}
|
|
8771
9154
|
console.log();
|