@elisym/cli 0.32.0 → 0.33.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 +572 -113
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --no-deprecation
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
|
-
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync,
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, statSync, realpathSync, renameSync, chmodSync, mkdirSync, writeFileSync, unlinkSync, appendFileSync, rmdirSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve, basename, extname, relative, sep } from 'node:path';
|
|
5
|
-
import { DEFAULTS, SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, splAssetsForNetwork, formatSol, formatAssetAmount, signerFromSecretKeyBase58, resolveUsdcAsset, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE,
|
|
6
|
-
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir,
|
|
5
|
+
import { DEFAULTS, SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, splAssetsForNetwork, formatSol, formatAssetAmount, deleteControlCharacters, signerFromSecretKeyBase58, resolveUsdcAsset, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, createBlossomTransport, generateSolanaWallet, verifyAgentIdentities, getProtocolConfig, getProtocolProgramId, calculateProtocolFee, LIMITS, makeCensor, DEFAULT_REDACT_PATHS, flattenForComparison, POLICY_T_TAG, SESSION_ID_REGEX, createSlidingWindowLimiter, excerptUntrusted, isScriptBillingExhaustedError, excerptUntrustedTail, scriptOutput, isScriptExecutionError, utf8ByteLength, readAcceptedTransports, resolveDelegationAsset, MAX_PROOF_TTL_SECS, PROOF_CLOCK_SKEW_SECS, verifyDelegationAuthProof, deriveOwnerDelegationAta, getDelegation, buildDelegatedTransfer, buildSignedPull, sendConfirmToTerminal, parseDelegatedPayment, confirmPullToTerminal, isDefinitelyUnpaid, decodeJobPayload, isLlmHealthError, BoundedSet, KIND_JOB_FEEDBACK, encodeSecretKeyBase58, GITHUB_USERNAME_REGEX, X_USERNAME_REGEX, normalizeNip05Identifier, splitNip05Identifier, clipToCodeUnits, degenerateReference, PROVIDER_REFUSED_PREFIX, PROVIDER_FAILED_MESSAGE, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
|
|
6
|
+
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, ensureGitignoreHasX402Entries, readMediaCache, ensureGitignoreHasPrivateStateEntries, loadPoliciesFromDir, ensureGitignoreHasDelegationNoncesEntry, ensureGitignoreHasIrohEntry, ensureGitignoreHasSessionsEntry, readAgentPublic, isBlockingNodeSync, AgentNameSchema, lookupCachedUrl, newCacheEntry, isBlockingNode, 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';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { parseSkillMd, validateSkillFrontmatter, isHostScratchError, isScriptRefusalError, startsWithRefusalHint, HostScratchError, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, loadSkillsFromDir, OnchainCallSkill, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
12
|
+
import { mkdtemp, writeFile, rm, readFile, mkdir, chmod, rename, readdir, stat } from 'node:fs/promises';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import Decimal from 'decimal.js-light';
|
|
15
15
|
import { decodePaymentRequiredHeader } from '@x402/core/http';
|
|
16
|
-
import { createHash } from 'node:crypto';
|
|
16
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
17
17
|
import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey } from '@elisym/sdk/llm-health';
|
|
18
18
|
import { createIrohTransport } from '@elisym/sdk/node';
|
|
19
19
|
import { lookup } from 'node:dns/promises';
|
|
@@ -1553,6 +1553,129 @@ function formatSplBalanceValue(asset, balance) {
|
|
|
1553
1553
|
async function fetchUsdcBalance(rpc, owner, network) {
|
|
1554
1554
|
return fetchSplBalance(rpc, owner, resolveUsdcAsset(network));
|
|
1555
1555
|
}
|
|
1556
|
+
async function findSharedPayoutNeighbors(cwd, ownDir, network, payoutAddress) {
|
|
1557
|
+
let ownReal;
|
|
1558
|
+
try {
|
|
1559
|
+
ownReal = realpathSync(ownDir);
|
|
1560
|
+
} catch {
|
|
1561
|
+
return [];
|
|
1562
|
+
}
|
|
1563
|
+
const listed = await listAgents(cwd);
|
|
1564
|
+
const candidates = [];
|
|
1565
|
+
for (const agent of listed) {
|
|
1566
|
+
candidates.push({ name: agent.name, dir: agent.dir, source: agent.source });
|
|
1567
|
+
if (agent.shadowsGlobal) {
|
|
1568
|
+
const homeDir = resolveInHome(agent.name);
|
|
1569
|
+
if (homeDir !== null) {
|
|
1570
|
+
candidates.push({ name: agent.name, dir: homeDir, source: "home" });
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
const neighbors = [];
|
|
1575
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1576
|
+
for (const candidate of candidates) {
|
|
1577
|
+
if (!AgentNameSchema.safeParse(candidate.name).success) {
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
let candidateReal;
|
|
1581
|
+
try {
|
|
1582
|
+
candidateReal = realpathSync(candidate.dir);
|
|
1583
|
+
} catch {
|
|
1584
|
+
continue;
|
|
1585
|
+
}
|
|
1586
|
+
if (candidateReal === ownReal || seen.has(candidateReal)) {
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
seen.add(candidateReal);
|
|
1590
|
+
let neighborAddress;
|
|
1591
|
+
let neighborNetwork;
|
|
1592
|
+
try {
|
|
1593
|
+
const { yaml } = await readAgentPublic({
|
|
1594
|
+
name: candidate.name,
|
|
1595
|
+
dir: candidate.dir,
|
|
1596
|
+
source: candidate.source,
|
|
1597
|
+
shadowsGlobal: false
|
|
1598
|
+
});
|
|
1599
|
+
const solana = yaml.payments.find((entry) => entry.chain === "solana");
|
|
1600
|
+
neighborAddress = solana?.address;
|
|
1601
|
+
neighborNetwork = solana?.network;
|
|
1602
|
+
} catch {
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
if (neighborAddress !== payoutAddress || neighborNetwork !== network) {
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const paid = await neighborPaidState(candidate.dir, network);
|
|
1609
|
+
if (paid === false) {
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
neighbors.push({
|
|
1613
|
+
name: candidate.name,
|
|
1614
|
+
// Sanitized, and newlines collapsed SEPARATELY: the sanitizer deletes
|
|
1615
|
+
// every C0/C1 control except tab and newline, and a newline in a POSIX
|
|
1616
|
+
// path is legal - left alone it would break the banner across lines. Two
|
|
1617
|
+
// different defenses, and neither substitutes for the other: the name is
|
|
1618
|
+
// checked by a pattern that admits nothing the sanitizer touches, while
|
|
1619
|
+
// the path carries an operator's project root that nobody validates.
|
|
1620
|
+
// `\s+` would be wrong here - it would eat the legitimate double spaces
|
|
1621
|
+
// and tabs a real path can carry, and the printed path would stop
|
|
1622
|
+
// matching the one on disk.
|
|
1623
|
+
dir: deleteControlCharacters(candidate.dir).replace(/\n+/g, " "),
|
|
1624
|
+
paid
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
return neighbors;
|
|
1628
|
+
}
|
|
1629
|
+
async function neighborPaidState(neighborDir, network) {
|
|
1630
|
+
const skillsDir = agentPaths(neighborDir).skills;
|
|
1631
|
+
let entries;
|
|
1632
|
+
try {
|
|
1633
|
+
entries = readdirSync(skillsDir);
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
return error.code === "ENOENT" ? false : "unknown";
|
|
1636
|
+
}
|
|
1637
|
+
let denominator = 0;
|
|
1638
|
+
let sawLess = false;
|
|
1639
|
+
for (const entry of entries) {
|
|
1640
|
+
const entryPath = join(skillsDir, entry);
|
|
1641
|
+
try {
|
|
1642
|
+
if (!statSync(entryPath).isDirectory()) {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
} catch {
|
|
1646
|
+
sawLess = true;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
const skillMd = join(entryPath, "SKILL.md");
|
|
1650
|
+
try {
|
|
1651
|
+
if (isBlockingNodeSync(skillMd)) {
|
|
1652
|
+
return "unknown";
|
|
1653
|
+
}
|
|
1654
|
+
readFileSync(skillMd, "utf-8");
|
|
1655
|
+
denominator += 1;
|
|
1656
|
+
} catch (error) {
|
|
1657
|
+
if (error.code !== "ENOENT") {
|
|
1658
|
+
sawLess = true;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
let skills;
|
|
1663
|
+
try {
|
|
1664
|
+
skills = loadSkillsFromDir(skillsDir, {
|
|
1665
|
+
network,
|
|
1666
|
+
allowFreeSkills: true,
|
|
1667
|
+
allowX402Skills: true,
|
|
1668
|
+
logger: { warn: () => {
|
|
1669
|
+
} }
|
|
1670
|
+
});
|
|
1671
|
+
} catch {
|
|
1672
|
+
return "unknown";
|
|
1673
|
+
}
|
|
1674
|
+
if (skills.some((skill) => skill.priceSubunits > 0n)) {
|
|
1675
|
+
return true;
|
|
1676
|
+
}
|
|
1677
|
+
return sawLess || skills.length < denominator ? "unknown" : false;
|
|
1678
|
+
}
|
|
1556
1679
|
|
|
1557
1680
|
// src/x402/constants.ts
|
|
1558
1681
|
var X402_SOLANA_DEVNET_CAIP2 = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1";
|
|
@@ -1787,13 +1910,10 @@ function fail(message) {
|
|
|
1787
1910
|
`);
|
|
1788
1911
|
process.exit(1);
|
|
1789
1912
|
}
|
|
1913
|
+
var CONTROL_CHARACTERS = /\p{Cc}/gu;
|
|
1914
|
+
var UNSEEN_CHARACTERS = /[\p{Cf}\p{Default_Ignorable_Code_Point}\uFFFD]/gu;
|
|
1790
1915
|
function sanitizeUpstreamText(raw, maxLength) {
|
|
1791
|
-
|
|
1792
|
-
for (const char of raw) {
|
|
1793
|
-
const code = char.codePointAt(0) ?? 0;
|
|
1794
|
-
withoutControls += code < 32 || code === 127 ? " " : char;
|
|
1795
|
-
}
|
|
1796
|
-
return withoutControls.replace(/—/g, "-").replace(/\s+/g, " ").trim().slice(0, maxLength).trim();
|
|
1916
|
+
return raw.normalize("NFKC").replace(CONTROL_CHARACTERS, " ").replace(UNSEEN_CHARACTERS, "").replace(/—/g, "-").replace(/\s+/g, " ").trim().slice(0, maxLength).trim();
|
|
1797
1917
|
}
|
|
1798
1918
|
function slugFromUrl(url) {
|
|
1799
1919
|
const pathPart = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean).pop() ?? "";
|
|
@@ -1892,6 +2012,12 @@ function scanExistingSkills(skillsDir, candidateDTag, url) {
|
|
|
1892
2012
|
if (!statSync(join(skillsDir, entry)).isDirectory()) {
|
|
1893
2013
|
continue;
|
|
1894
2014
|
}
|
|
2015
|
+
if (isBlockingNodeSync(skillMdPath)) {
|
|
2016
|
+
console.warn(
|
|
2017
|
+
` ! Could not check ${skillMdPath} for a name collision: it is a pipe, socket or device, not a file`
|
|
2018
|
+
);
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
1895
2021
|
const { frontmatter } = parseSkillMd(readFileSync(skillMdPath, "utf-8"));
|
|
1896
2022
|
if (typeof frontmatter.name !== "string") {
|
|
1897
2023
|
continue;
|
|
@@ -1928,6 +2054,16 @@ function describeUnacceptableAccepts(probe, agentNetwork) {
|
|
|
1928
2054
|
}
|
|
1929
2055
|
return `no exact-scheme ${agentNetwork}-USDC requirement found (service accepts: ${networks.join(", ") || "nothing parseable"})`;
|
|
1930
2056
|
}
|
|
2057
|
+
async function writeSkillMdRefusingBlockingNode(targetDir, content) {
|
|
2058
|
+
await mkdir(targetDir, { recursive: true, mode: 448 });
|
|
2059
|
+
await chmod(targetDir, 448).catch(() => void 0);
|
|
2060
|
+
const skillMdPath = join(targetDir, "SKILL.md");
|
|
2061
|
+
if (isBlockingNodeSync(skillMdPath)) {
|
|
2062
|
+
throw new Error(`Refusing to write ${skillMdPath}: it is a pipe, socket or device, not a file`);
|
|
2063
|
+
}
|
|
2064
|
+
await writeFile(skillMdPath, content, "utf-8");
|
|
2065
|
+
return skillMdPath;
|
|
2066
|
+
}
|
|
1931
2067
|
async function cmdX402Add(url, agentName, options) {
|
|
1932
2068
|
const cwd = process.cwd();
|
|
1933
2069
|
const { default: inquirer } = await import('inquirer');
|
|
@@ -2242,8 +2378,7 @@ async function cmdX402Add(url, agentName, options) {
|
|
|
2242
2378
|
allowFreeSkills: false,
|
|
2243
2379
|
allowX402Skills: true
|
|
2244
2380
|
});
|
|
2245
|
-
await
|
|
2246
|
-
await writeFile(join(targetDir, "SKILL.md"), content, "utf-8");
|
|
2381
|
+
await writeSkillMdRefusingBlockingNode(targetDir, content);
|
|
2247
2382
|
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
2248
2383
|
console.log(`
|
|
2249
2384
|
Wrote ${join(targetDir, "SKILL.md")}`);
|
|
@@ -3279,6 +3414,9 @@ async function probeRelays(relays, logger, timeoutMs = TCP_PROBE_TIMEOUT_MS) {
|
|
|
3279
3414
|
}
|
|
3280
3415
|
var LEDGER_DIR_MODE = 448;
|
|
3281
3416
|
var LEDGER_FILE_MODE = 384;
|
|
3417
|
+
function isUsableSignature(value) {
|
|
3418
|
+
return typeof value === "string" && value.length > 0;
|
|
3419
|
+
}
|
|
3282
3420
|
var VALID_TRANSITIONS = {
|
|
3283
3421
|
paid: ["executed", "failed"],
|
|
3284
3422
|
executed: ["delivered", "failed"],
|
|
@@ -3286,6 +3424,30 @@ var VALID_TRANSITIONS = {
|
|
|
3286
3424
|
failed: []
|
|
3287
3425
|
};
|
|
3288
3426
|
var MAX_DOUBLE_SETTLE_WARNINGS = 20;
|
|
3427
|
+
var STRANDED_TEMP_MIN_AGE_MS = 60 * 60 * 1e3;
|
|
3428
|
+
function sweepStrandedTemporaries(path) {
|
|
3429
|
+
const legacyName = `${basename(path)}.tmp`;
|
|
3430
|
+
const prefix = `${legacyName}.`;
|
|
3431
|
+
const cutoff = Date.now() - STRANDED_TEMP_MIN_AGE_MS;
|
|
3432
|
+
let names;
|
|
3433
|
+
try {
|
|
3434
|
+
names = readdirSync(dirname(path));
|
|
3435
|
+
} catch {
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
for (const name of names) {
|
|
3439
|
+
if (name !== legacyName && !name.startsWith(prefix)) {
|
|
3440
|
+
continue;
|
|
3441
|
+
}
|
|
3442
|
+
const candidate = join(dirname(path), name);
|
|
3443
|
+
try {
|
|
3444
|
+
if (statSync(candidate).mtimeMs < cutoff) {
|
|
3445
|
+
unlinkSync(candidate);
|
|
3446
|
+
}
|
|
3447
|
+
} catch {
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3289
3451
|
var JobLedger = class {
|
|
3290
3452
|
entries = /* @__PURE__ */ new Map();
|
|
3291
3453
|
/**
|
|
@@ -3303,8 +3465,14 @@ var JobLedger = class {
|
|
|
3303
3465
|
constructor(ledgerPath) {
|
|
3304
3466
|
this.path = ledgerPath;
|
|
3305
3467
|
this.load();
|
|
3468
|
+
sweepStrandedTemporaries(this.path);
|
|
3306
3469
|
}
|
|
3307
3470
|
load() {
|
|
3471
|
+
if (isBlockingNodeSync(this.path)) {
|
|
3472
|
+
throw new Error(
|
|
3473
|
+
`Refusing to read the job ledger at ${this.path}: it is a pipe, socket or device, not a file. An empty ledger would drop the record of which transaction paid for which job.`
|
|
3474
|
+
);
|
|
3475
|
+
}
|
|
3308
3476
|
try {
|
|
3309
3477
|
const raw = readFileSync(this.path, "utf-8");
|
|
3310
3478
|
const data = JSON.parse(raw);
|
|
@@ -3327,6 +3495,11 @@ var JobLedger = class {
|
|
|
3327
3495
|
);
|
|
3328
3496
|
}
|
|
3329
3497
|
} catch (e) {
|
|
3498
|
+
if (typeof e?.code === "string" && e.code !== "ENOENT") {
|
|
3499
|
+
throw new Error(
|
|
3500
|
+
`Refusing to start on a job ledger that cannot be read (${e.code}) at ${this.path}. An empty ledger would drop the record of which transaction paid for which job. Check the file's owner and mode (a ledger written under sudo needs a chown).`
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3330
3503
|
if (e?.code !== "ENOENT") {
|
|
3331
3504
|
console.warn(` ! Ledger load warning: ${e?.message ?? "unknown error"}`);
|
|
3332
3505
|
try {
|
|
@@ -3343,10 +3516,18 @@ var JobLedger = class {
|
|
|
3343
3516
|
const dir = dirname(this.path);
|
|
3344
3517
|
mkdirSync(dir, { recursive: true, mode: LEDGER_DIR_MODE });
|
|
3345
3518
|
const obj = Object.fromEntries(this.entries);
|
|
3346
|
-
const tmp = this.path
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3519
|
+
const tmp = `${this.path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
3520
|
+
try {
|
|
3521
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2), { mode: LEDGER_FILE_MODE });
|
|
3522
|
+
chmodSync(tmp, LEDGER_FILE_MODE);
|
|
3523
|
+
renameSync(tmp, this.path);
|
|
3524
|
+
} catch (error) {
|
|
3525
|
+
try {
|
|
3526
|
+
unlinkSync(tmp);
|
|
3527
|
+
} catch {
|
|
3528
|
+
}
|
|
3529
|
+
throw error;
|
|
3530
|
+
}
|
|
3350
3531
|
}
|
|
3351
3532
|
recordPaid(entry) {
|
|
3352
3533
|
if (this.entries.has(entry.job_id)) {
|
|
@@ -3414,7 +3595,7 @@ var JobLedger = class {
|
|
|
3414
3595
|
let suppressed = 0;
|
|
3415
3596
|
for (const entry of this.entries.values()) {
|
|
3416
3597
|
const paymentSignature = entry.payment_signature;
|
|
3417
|
-
if (
|
|
3598
|
+
if (!isUsableSignature(paymentSignature)) {
|
|
3418
3599
|
continue;
|
|
3419
3600
|
}
|
|
3420
3601
|
const owner = this.paymentSignatureOwners.get(paymentSignature);
|
|
@@ -3484,6 +3665,9 @@ var JobLedger = class {
|
|
|
3484
3665
|
* this job - refusing the owner its own settlement until the next restart.
|
|
3485
3666
|
*/
|
|
3486
3667
|
claimPaymentSignature(paymentSignature, jobId) {
|
|
3668
|
+
if (!isUsableSignature(paymentSignature)) {
|
|
3669
|
+
return "unknown-job";
|
|
3670
|
+
}
|
|
3487
3671
|
const entry = this.entries.get(jobId);
|
|
3488
3672
|
if (entry === void 0) {
|
|
3489
3673
|
return "unknown-job";
|
|
@@ -3603,7 +3787,7 @@ var JobLedger = class {
|
|
|
3603
3787
|
for (const [id, entry] of this.entries) {
|
|
3604
3788
|
if ((entry.status === "delivered" || entry.status === "failed") && entry.created_at < cutoff) {
|
|
3605
3789
|
this.entries.delete(id);
|
|
3606
|
-
if (
|
|
3790
|
+
if (isUsableSignature(entry.payment_signature)) {
|
|
3607
3791
|
prunedSignatures.add(entry.payment_signature);
|
|
3608
3792
|
}
|
|
3609
3793
|
deleted += 1;
|
|
@@ -3651,8 +3835,14 @@ var UsedNonceStore = class {
|
|
|
3651
3835
|
this.path = noncePath;
|
|
3652
3836
|
this.maxEntries = maxEntries;
|
|
3653
3837
|
this.load();
|
|
3838
|
+
sweepStrandedTemporaries(this.path);
|
|
3654
3839
|
}
|
|
3655
3840
|
load() {
|
|
3841
|
+
if (isBlockingNodeSync(this.path)) {
|
|
3842
|
+
throw new Error(
|
|
3843
|
+
`Refusing to read the nonce store at ${this.path}: it is a pipe, socket or device, not a file. An empty store would let a delegated pull be replayed.`
|
|
3844
|
+
);
|
|
3845
|
+
}
|
|
3656
3846
|
try {
|
|
3657
3847
|
const raw = readFileSync(this.path, "utf-8");
|
|
3658
3848
|
const data = JSON.parse(raw);
|
|
@@ -3662,6 +3852,11 @@ var UsedNonceStore = class {
|
|
|
3662
3852
|
}
|
|
3663
3853
|
}
|
|
3664
3854
|
} catch (e) {
|
|
3855
|
+
if (typeof e?.code === "string" && e.code !== "ENOENT") {
|
|
3856
|
+
throw new Error(
|
|
3857
|
+
`Refusing to start on a nonce store that cannot be read (${e.code}) at ${this.path}. An empty store would let a delegated pull be replayed. Check the file's owner and mode (a store written under sudo needs a chown).`
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3665
3860
|
if (e?.code !== "ENOENT") {
|
|
3666
3861
|
console.warn(` ! Nonce store load warning: ${e?.message ?? "unknown error"}`);
|
|
3667
3862
|
try {
|
|
@@ -3677,9 +3872,17 @@ var UsedNonceStore = class {
|
|
|
3677
3872
|
const dir = dirname(this.path);
|
|
3678
3873
|
mkdirSync(dir, { recursive: true, mode: LEDGER_DIR_MODE });
|
|
3679
3874
|
const obj = Object.fromEntries(this.entries);
|
|
3680
|
-
const tmp = this.path
|
|
3681
|
-
|
|
3682
|
-
|
|
3875
|
+
const tmp = `${this.path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
3876
|
+
try {
|
|
3877
|
+
writeFileSync(tmp, JSON.stringify(obj), { mode: LEDGER_FILE_MODE });
|
|
3878
|
+
renameSync(tmp, this.path);
|
|
3879
|
+
} catch (error) {
|
|
3880
|
+
try {
|
|
3881
|
+
unlinkSync(tmp);
|
|
3882
|
+
} catch {
|
|
3883
|
+
}
|
|
3884
|
+
throw error;
|
|
3885
|
+
}
|
|
3683
3886
|
chmodSync(this.path, LEDGER_FILE_MODE);
|
|
3684
3887
|
}
|
|
3685
3888
|
has(key) {
|
|
@@ -3863,6 +4066,43 @@ var MIME_BY_EXT = {
|
|
|
3863
4066
|
function mimeFromPath(path) {
|
|
3864
4067
|
return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
3865
4068
|
}
|
|
4069
|
+
var IMAGE_EXTENSIONS = Object.keys(MIME_BY_EXT);
|
|
4070
|
+
function isImagePath(path) {
|
|
4071
|
+
return Object.hasOwn(MIME_BY_EXT, extname(path).toLowerCase());
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
// src/rpc-redact.ts
|
|
4075
|
+
var PUBLIC_SOLANA_RPC_HOSTS = /* @__PURE__ */ new Set([
|
|
4076
|
+
"api.devnet.solana.com",
|
|
4077
|
+
"api.mainnet-beta.solana.com",
|
|
4078
|
+
"api.testnet.solana.com"
|
|
4079
|
+
]);
|
|
4080
|
+
function stripRpcSecrets(raw) {
|
|
4081
|
+
try {
|
|
4082
|
+
const parsed = new URL(raw);
|
|
4083
|
+
parsed.username = "";
|
|
4084
|
+
parsed.password = "";
|
|
4085
|
+
if (!PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname)) {
|
|
4086
|
+
return `${parsed.protocol}//${parsed.host}/***`;
|
|
4087
|
+
}
|
|
4088
|
+
const marker = parsed.search.length > 0 ? "?***" : "";
|
|
4089
|
+
parsed.search = "";
|
|
4090
|
+
return `${parsed.toString()}${marker}`;
|
|
4091
|
+
} catch {
|
|
4092
|
+
return "[unparseable RPC URL]";
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
function isPublicSolanaRpcUrl(raw) {
|
|
4096
|
+
try {
|
|
4097
|
+
const parsed = new URL(raw);
|
|
4098
|
+
return PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname) && !parsed.username && !parsed.password && !parsed.search && (parsed.pathname === "" || parsed.pathname === "/");
|
|
4099
|
+
} catch {
|
|
4100
|
+
return false;
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
function redactRpcUrlsInText(text) {
|
|
4104
|
+
return text.replace(/https?:\/\/[^\s)'"]+/g, (url) => stripRpcSecrets(url));
|
|
4105
|
+
}
|
|
3866
4106
|
var REFERENCE_SCAN_WINDOW = DEFAULTS.VERIFY_SIGNATURE_LIMIT;
|
|
3867
4107
|
var REFERENCE_SCAN_LIST_ATTEMPTS = 3;
|
|
3868
4108
|
var REFERENCE_SCAN_LIST_RETRY_DELAY_MS = 400;
|
|
@@ -4247,7 +4487,7 @@ var PaymentRecovery = class {
|
|
|
4247
4487
|
* An error is never "the customer did not pay" - the caller keeps the job
|
|
4248
4488
|
* recoverable.
|
|
4249
4489
|
*/
|
|
4250
|
-
async listReferenceCandidates(reference, rpc,
|
|
4490
|
+
async listReferenceCandidates(reference, rpc, pastDeadline) {
|
|
4251
4491
|
let lastMessage = "unknown error";
|
|
4252
4492
|
let attempts = 0;
|
|
4253
4493
|
for (let attempt = 0; attempt < REFERENCE_SCAN_LIST_ATTEMPTS; attempt++) {
|
|
@@ -4263,7 +4503,7 @@ var PaymentRecovery = class {
|
|
|
4263
4503
|
};
|
|
4264
4504
|
} catch (e) {
|
|
4265
4505
|
lastMessage = e?.message ?? "unknown error";
|
|
4266
|
-
if (
|
|
4506
|
+
if (pastDeadline()) {
|
|
4267
4507
|
break;
|
|
4268
4508
|
}
|
|
4269
4509
|
if (attempt < REFERENCE_SCAN_LIST_ATTEMPTS - 1) {
|
|
@@ -4303,7 +4543,9 @@ var PaymentRecovery = class {
|
|
|
4303
4543
|
* the SDK default of 10 retries x 3s x a full window - over ten minutes for
|
|
4304
4544
|
* one deferred entry.
|
|
4305
4545
|
*
|
|
4306
|
-
* TERMINAL ("none") REQUIRES ALL
|
|
4546
|
+
* TERMINAL ("none") REQUIRES ALL FOUR OF THESE, and the caller adds four more
|
|
4547
|
+
* on top (they are counted as six in the docs, where 2-4 below are read as one
|
|
4548
|
+
* condition - "nothing was skipped and nothing was left unverified"):
|
|
4307
4549
|
* 1. the listing SUCCEEDED and came back SHORT of the window, so this is the
|
|
4308
4550
|
* reference's whole history rather than a truncated page. A flood that
|
|
4309
4551
|
* fills the window could be hiding the payment behind it, which is
|
|
@@ -4315,26 +4557,40 @@ var PaymentRecovery = class {
|
|
|
4315
4557
|
* evidence of non-payment;
|
|
4316
4558
|
* 3. no candidate was SKIPPED for belonging to another job. A skip means
|
|
4317
4559
|
* something did touch this reference and we chose not to look at it, so
|
|
4318
|
-
* the scan saw less than the whole truth
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4321
|
-
*
|
|
4560
|
+
* the scan saw less than the whole truth;
|
|
4561
|
+
* 4. no candidate was SKIPPED for an unusable signature. Same reasoning as
|
|
4562
|
+
* 3, with the node rather than the ledger as the reason we did not look:
|
|
4563
|
+
* a blanked signature is still a transaction on this reference.
|
|
4564
|
+
* The caller then requires, in this order: that the job owns no settlement of
|
|
4565
|
+
* its own, that the payment request's own expiry has passed, that the endpoint
|
|
4566
|
+
* proved its cluster and that it serves address history, and - one level up,
|
|
4567
|
+
* in `runtime.ts` - a second consecutive sighting. Anything else is
|
|
4568
|
+
* inconclusive: a false deferral costs latency, a false "unpaid" destroys the
|
|
4569
|
+
* customer's money.
|
|
4322
4570
|
*
|
|
4323
4571
|
* What "none" therefore means is narrow and honest: after the failed-on-chain
|
|
4324
4572
|
* transactions are dropped, the reference's entire history is EMPTY - there
|
|
4325
4573
|
* was nothing at all to look at.
|
|
4326
4574
|
*/
|
|
4327
|
-
async verifyByReferenceScan(reference, rpc, jobId,
|
|
4328
|
-
|
|
4575
|
+
async verifyByReferenceScan(reference, rpc, jobId, pastDeadline, verifySignature) {
|
|
4576
|
+
if (pastDeadline()) {
|
|
4577
|
+
return { outcome: "inconclusive", error: "the reference scan ran out of time" };
|
|
4578
|
+
}
|
|
4579
|
+
const listed = await this.listReferenceCandidates(reference, rpc, pastDeadline);
|
|
4329
4580
|
if ("error" in listed) {
|
|
4330
4581
|
return { outcome: "inconclusive", error: listed.error };
|
|
4331
4582
|
}
|
|
4332
4583
|
let skippedConsumed = false;
|
|
4584
|
+
let skippedUnusable = false;
|
|
4333
4585
|
let unverifiableCandidate;
|
|
4334
4586
|
for (const candidate of listed.signatures) {
|
|
4335
|
-
if (
|
|
4587
|
+
if (pastDeadline()) {
|
|
4336
4588
|
return { outcome: "inconclusive", error: "the reference scan ran out of time" };
|
|
4337
4589
|
}
|
|
4590
|
+
if (!isUsableSignature(candidate)) {
|
|
4591
|
+
skippedUnusable = true;
|
|
4592
|
+
continue;
|
|
4593
|
+
}
|
|
4338
4594
|
const owner = this.ledger.paymentSignatureOwner(candidate);
|
|
4339
4595
|
if (owner !== void 0 && owner !== jobId) {
|
|
4340
4596
|
skippedConsumed = true;
|
|
@@ -4346,6 +4602,9 @@ var PaymentRecovery = class {
|
|
|
4346
4602
|
}
|
|
4347
4603
|
unverifiableCandidate ??= verified.error ?? "no reason given";
|
|
4348
4604
|
}
|
|
4605
|
+
if (pastDeadline()) {
|
|
4606
|
+
return { outcome: "inconclusive", error: "the reference scan ran out of time" };
|
|
4607
|
+
}
|
|
4349
4608
|
if (listed.windowFull) {
|
|
4350
4609
|
return {
|
|
4351
4610
|
outcome: "inconclusive",
|
|
@@ -4358,6 +4617,12 @@ var PaymentRecovery = class {
|
|
|
4358
4617
|
error: "the reference carries a transaction another job already settled, so this scan did not see the whole picture"
|
|
4359
4618
|
};
|
|
4360
4619
|
}
|
|
4620
|
+
if (skippedUnusable) {
|
|
4621
|
+
return {
|
|
4622
|
+
outcome: "inconclusive",
|
|
4623
|
+
error: "the reference carries a transaction whose signature the node reported unusably, so this scan did not see the whole picture"
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4361
4626
|
if (unverifiableCandidate !== void 0) {
|
|
4362
4627
|
return {
|
|
4363
4628
|
outcome: "inconclusive",
|
|
@@ -4405,10 +4670,25 @@ var PaymentRecovery = class {
|
|
|
4405
4670
|
);
|
|
4406
4671
|
return "corrupt-state";
|
|
4407
4672
|
}
|
|
4408
|
-
|
|
4673
|
+
let deadline;
|
|
4409
4674
|
try {
|
|
4410
4675
|
const rpc = createSolanaRpc(getRpcUrl(this.network));
|
|
4411
4676
|
const protocolConfig = await this.fetchProtocolConfig();
|
|
4677
|
+
deadline = Date.now() + REFERENCE_SCAN_DEADLINE_MS;
|
|
4678
|
+
const pastDeadline = () => signal?.aborted === true || Date.now() >= deadline;
|
|
4679
|
+
const degenerate = await degenerateReference(request, this.network, protocolConfig.treasury);
|
|
4680
|
+
if (degenerate !== void 0) {
|
|
4681
|
+
if (!isUsableSignature(entry.payment_signature)) {
|
|
4682
|
+
log(
|
|
4683
|
+
`[${shortId}] Recovery: the payment request's reference (${request.reference}) is an address the payment itself is computed from, so the transfer cannot be singled out by listing it. This is provider-side state, not a chain or customer problem.`
|
|
4684
|
+
);
|
|
4685
|
+
return "corrupt-state";
|
|
4686
|
+
}
|
|
4687
|
+
log(
|
|
4688
|
+
`[${shortId}] Recovery: the reference (${request.reference}) is an address the payment is computed from, but this job already owns settlement ${entry.payment_signature} - deferring rather than failing it, in case this build's list grew past what the settlement was accepted under. Not listing the reference: it cannot single out a payment.`
|
|
4689
|
+
);
|
|
4690
|
+
return "deferred";
|
|
4691
|
+
}
|
|
4412
4692
|
const verify = async (txSignature2, retries) => {
|
|
4413
4693
|
const verification = this.strategy.verifyPayment(rpc, request, protocolConfig, {
|
|
4414
4694
|
txSignature: txSignature2,
|
|
@@ -4440,7 +4720,7 @@ var PaymentRecovery = class {
|
|
|
4440
4720
|
};
|
|
4441
4721
|
const ownSignature = entry.payment_signature;
|
|
4442
4722
|
let txSignature;
|
|
4443
|
-
if (ownSignature
|
|
4723
|
+
if (isUsableSignature(ownSignature) && !pastDeadline()) {
|
|
4444
4724
|
const own = await verify(ownSignature, OWN_SETTLEMENT_VERIFY_RETRIES);
|
|
4445
4725
|
if (own.verified) {
|
|
4446
4726
|
txSignature = ownSignature;
|
|
@@ -4455,11 +4735,11 @@ var PaymentRecovery = class {
|
|
|
4455
4735
|
reference,
|
|
4456
4736
|
rpc,
|
|
4457
4737
|
entry.job_id,
|
|
4458
|
-
|
|
4738
|
+
pastDeadline,
|
|
4459
4739
|
verify
|
|
4460
4740
|
);
|
|
4461
4741
|
if (scan.outcome === "none") {
|
|
4462
|
-
if (ownSignature
|
|
4742
|
+
if (isUsableSignature(ownSignature)) {
|
|
4463
4743
|
log(
|
|
4464
4744
|
`[${shortId}] Recovery: the reference scan is empty, but this job already owns settlement ${ownSignature} - treating the empty scan as an RPC history gap, not as non-payment. Deferring.`
|
|
4465
4745
|
);
|
|
@@ -4800,14 +5080,24 @@ var SessionStore = class {
|
|
|
4800
5080
|
}
|
|
4801
5081
|
const payload = lines.map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
4802
5082
|
const path = this.sessionPath(customerId, sessionId);
|
|
5083
|
+
if (isBlockingNodeSync(path)) {
|
|
5084
|
+
this.log(`[sessions] session file is not a regular file; not recording this exchange`);
|
|
5085
|
+
return;
|
|
5086
|
+
}
|
|
4803
5087
|
try {
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
5088
|
+
try {
|
|
5089
|
+
appendFileSync(path, payload, { mode: FILE_MODE });
|
|
5090
|
+
} catch (error) {
|
|
5091
|
+
if (error.code !== "ENOENT") {
|
|
5092
|
+
throw error;
|
|
5093
|
+
}
|
|
5094
|
+
mkdirSync(join(this.root, customerId), { recursive: true, mode: DIR_MODE });
|
|
5095
|
+
appendFileSync(path, payload, { mode: FILE_MODE });
|
|
4808
5096
|
}
|
|
4809
|
-
|
|
4810
|
-
|
|
5097
|
+
} catch (error) {
|
|
5098
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5099
|
+
this.log(`[sessions] could not record this exchange (${reason}); delivering without it`);
|
|
5100
|
+
return;
|
|
4811
5101
|
}
|
|
4812
5102
|
this.globalBytes += Buffer.byteLength(payload);
|
|
4813
5103
|
}
|
|
@@ -4879,6 +5169,9 @@ var SessionStore = class {
|
|
|
4879
5169
|
let raw;
|
|
4880
5170
|
let size;
|
|
4881
5171
|
try {
|
|
5172
|
+
if (isBlockingNodeSync(path)) {
|
|
5173
|
+
return null;
|
|
5174
|
+
}
|
|
4882
5175
|
size = statSync(path).size;
|
|
4883
5176
|
raw = readFileSync(path, "utf-8");
|
|
4884
5177
|
} catch {
|
|
@@ -4910,9 +5203,17 @@ var SessionStore = class {
|
|
|
4910
5203
|
}
|
|
4911
5204
|
if (torn) {
|
|
4912
5205
|
const payload = lines.map((line) => JSON.stringify(line)).join("\n") + (lines.length > 0 ? "\n" : "");
|
|
4913
|
-
const tempPath = `${path}.tmp`;
|
|
4914
|
-
|
|
4915
|
-
|
|
5206
|
+
const tempPath = `${path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
5207
|
+
try {
|
|
5208
|
+
writeFileSync(tempPath, payload, { mode: FILE_MODE });
|
|
5209
|
+
renameSync(tempPath, path);
|
|
5210
|
+
} catch (error) {
|
|
5211
|
+
try {
|
|
5212
|
+
unlinkSync(tempPath);
|
|
5213
|
+
} catch {
|
|
5214
|
+
}
|
|
5215
|
+
throw error;
|
|
5216
|
+
}
|
|
4916
5217
|
this.globalBytes = Math.max(0, this.globalBytes - size + Buffer.byteLength(payload));
|
|
4917
5218
|
}
|
|
4918
5219
|
return lines;
|
|
@@ -4981,10 +5282,18 @@ var SessionStore = class {
|
|
|
4981
5282
|
const payload = rewritten.map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
4982
5283
|
const path = this.sessionPath(customerId, sessionId);
|
|
4983
5284
|
const oldSize = existsSync(path) ? statSync(path).size : 0;
|
|
4984
|
-
const tempPath = `${path}.tmp`;
|
|
5285
|
+
const tempPath = `${path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
4985
5286
|
mkdirSync(join(this.root, customerId), { recursive: true, mode: DIR_MODE });
|
|
4986
|
-
|
|
4987
|
-
|
|
5287
|
+
try {
|
|
5288
|
+
writeFileSync(tempPath, payload, { mode: FILE_MODE });
|
|
5289
|
+
renameSync(tempPath, path);
|
|
5290
|
+
} catch (error) {
|
|
5291
|
+
try {
|
|
5292
|
+
unlinkSync(tempPath);
|
|
5293
|
+
} catch {
|
|
5294
|
+
}
|
|
5295
|
+
throw error;
|
|
5296
|
+
}
|
|
4988
5297
|
this.globalBytes = Math.max(0, this.globalBytes - oldSize + Buffer.byteLength(payload));
|
|
4989
5298
|
return assembleReplay(rewritten, void 0);
|
|
4990
5299
|
}
|
|
@@ -5121,7 +5430,7 @@ var SessionStore = class {
|
|
|
5121
5430
|
continue;
|
|
5122
5431
|
}
|
|
5123
5432
|
for (const name of names) {
|
|
5124
|
-
if (!name.includes(".corrupt.")) {
|
|
5433
|
+
if (!name.includes(".corrupt.") && !name.includes(".tmp")) {
|
|
5125
5434
|
continue;
|
|
5126
5435
|
}
|
|
5127
5436
|
const path = join(dir, name);
|
|
@@ -5287,6 +5596,15 @@ var X402PermanentError = class extends Error {
|
|
|
5287
5596
|
};
|
|
5288
5597
|
|
|
5289
5598
|
// src/runtime.ts
|
|
5599
|
+
var MAX_LOGGED_MIME_CHARS = 128;
|
|
5600
|
+
function ledgerAmount(subunits) {
|
|
5601
|
+
if (subunits < 0n || subunits > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
5602
|
+
throw new Error(
|
|
5603
|
+
`Amount ${subunits.toString()} subunits cannot be recorded exactly: the ledger holds amounts as numbers, and this one is outside the range a number represents without rounding.`
|
|
5604
|
+
);
|
|
5605
|
+
}
|
|
5606
|
+
return Number(subunits);
|
|
5607
|
+
}
|
|
5290
5608
|
var payment = new SolanaPaymentStrategy();
|
|
5291
5609
|
var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
5292
5610
|
var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
|
|
@@ -5943,8 +6261,31 @@ var AgentRuntime = class {
|
|
|
5943
6261
|
});
|
|
5944
6262
|
return { feeBps: config.feeBps, treasury: config.treasury };
|
|
5945
6263
|
}
|
|
6264
|
+
/**
|
|
6265
|
+
* The ONE way a line leaves this runtime for the operator.
|
|
6266
|
+
*
|
|
6267
|
+
* Every line is scrubbed of RPC credentials here, once, rather than at the
|
|
6268
|
+
* call sites that happen to quote an error. Third-party RPC providers carry
|
|
6269
|
+
* the API key in the URL, and an error reaching this file can quote that URL:
|
|
6270
|
+
* `@solana/kit` does not put it in its own transport errors - measured across
|
|
6271
|
+
* refused connections, DNS failures, 429, 401, a JSON-RPC error and a body
|
|
6272
|
+
* that is not JSON - but for a JSON-RPC error it does not recognize it passes
|
|
6273
|
+
* the SERVER's message through, and a proxy is free to write the request URL
|
|
6274
|
+
* into that. `start.ts` already scrubbed the wallet error for this reason and
|
|
6275
|
+
* nothing here did.
|
|
6276
|
+
*
|
|
6277
|
+
* One place because the call sites are many and grow: seven bound a logger
|
|
6278
|
+
* of their own, and the payment path alone quotes an error in four. Nothing
|
|
6279
|
+
* this runtime logs carries a URL of its own, so the scrub costs no
|
|
6280
|
+
* diagnostic. The sink is `onLog`, which `start` fans out to the terminal and
|
|
6281
|
+
* to the structured log - so both are covered by the one pass.
|
|
6282
|
+
*/
|
|
6283
|
+
get operatorLog() {
|
|
6284
|
+
const sink = this.callbacks.onLog ?? console.log;
|
|
6285
|
+
return (line) => sink(redactRpcUrlsInText(line));
|
|
6286
|
+
}
|
|
5946
6287
|
async run() {
|
|
5947
|
-
const log = this.
|
|
6288
|
+
const log = this.operatorLog;
|
|
5948
6289
|
this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
|
|
5949
6290
|
this.sessionStore?.init();
|
|
5950
6291
|
this.reconcileUsedNonces();
|
|
@@ -6094,7 +6435,7 @@ var AgentRuntime = class {
|
|
|
6094
6435
|
this.callbacks.onStop?.();
|
|
6095
6436
|
} catch (error) {
|
|
6096
6437
|
const msg = error instanceof Error ? error.message : String(error);
|
|
6097
|
-
|
|
6438
|
+
this.operatorLog(`onStop error: ${msg}`);
|
|
6098
6439
|
}
|
|
6099
6440
|
this.abortController.abort();
|
|
6100
6441
|
for (const controller of this.jobAbortControllers) {
|
|
@@ -6132,7 +6473,7 @@ var AgentRuntime = class {
|
|
|
6132
6473
|
await this.executeJob(job, jobAbort.signal);
|
|
6133
6474
|
} catch (raw) {
|
|
6134
6475
|
const e = asHostScratchFailure(raw) ?? raw;
|
|
6135
|
-
const log = this.
|
|
6476
|
+
const log = this.operatorLog;
|
|
6136
6477
|
const operatorMessage = describeForOperator(e);
|
|
6137
6478
|
log(`[${job.jobId.slice(0, 8)}] Error: ${operatorMessage}`);
|
|
6138
6479
|
if (isHostScratchError(e)) {
|
|
@@ -6165,7 +6506,7 @@ var AgentRuntime = class {
|
|
|
6165
6506
|
}
|
|
6166
6507
|
/** Core job processing logic - payment, skill execution, result delivery. */
|
|
6167
6508
|
async executeJob(job, signal) {
|
|
6168
|
-
const log = this.
|
|
6509
|
+
const log = this.operatorLog;
|
|
6169
6510
|
const inputBytes = utf8ByteLength(job.input);
|
|
6170
6511
|
if (inputBytes > LIMITS.MAX_INPUT_LENGTH) {
|
|
6171
6512
|
throw new Error(`Input too long: ${inputBytes} bytes (max ${LIMITS.MAX_INPUT_LENGTH})`);
|
|
@@ -6200,7 +6541,11 @@ var AgentRuntime = class {
|
|
|
6200
6541
|
const attachmentOk = method === "POST" && job.attachment.mime.startsWith("text/") && job.attachment.size <= maxInputBytes;
|
|
6201
6542
|
if (!attachmentOk) {
|
|
6202
6543
|
log(
|
|
6203
|
-
`
|
|
6544
|
+
// `mime` is the customer's string and its schema bounds only the
|
|
6545
|
+
// length, so a newline in it forged whole log lines. Every other
|
|
6546
|
+
// piece of untrusted text in this file already goes through the
|
|
6547
|
+
// excerpt; this was the one that did not.
|
|
6548
|
+
`[${job.jobId.slice(0, 8)}] Rejecting attachment on x402 skill (mime=${excerptUntrusted(job.attachment.mime, MAX_LOGGED_MIME_CHARS)}, size=${job.attachment.size})`
|
|
6204
6549
|
);
|
|
6205
6550
|
await this.transport.sendFeedback(job, {
|
|
6206
6551
|
type: "error",
|
|
@@ -6253,7 +6598,7 @@ var AgentRuntime = class {
|
|
|
6253
6598
|
if (delegatedPull === void 0) {
|
|
6254
6599
|
return;
|
|
6255
6600
|
}
|
|
6256
|
-
netAmount =
|
|
6601
|
+
netAmount = ledgerAmount(delegatedPull.priceSubunits);
|
|
6257
6602
|
} else if (jobPrice > 0) {
|
|
6258
6603
|
const result = await this.collectPayment(job, jobPrice, jobAsset, signal);
|
|
6259
6604
|
netAmount = result.netAmount;
|
|
@@ -6377,7 +6722,7 @@ var AgentRuntime = class {
|
|
|
6377
6722
|
log
|
|
6378
6723
|
);
|
|
6379
6724
|
const outcome = await this.executeDelegatedPull(job, delegatedPull, log);
|
|
6380
|
-
netAmount =
|
|
6725
|
+
netAmount = ledgerAmount(delegatedPull.chargedSubunits ?? delegatedPull.priceSubunits);
|
|
6381
6726
|
if (outcome === "dead") {
|
|
6382
6727
|
this.ledger.markFailed(job.jobId);
|
|
6383
6728
|
await this.transport.sendFeedback(job, {
|
|
@@ -6634,7 +6979,7 @@ var AgentRuntime = class {
|
|
|
6634
6979
|
job.jobId,
|
|
6635
6980
|
pull.signature,
|
|
6636
6981
|
Number(pull.lastValidBlockHeight),
|
|
6637
|
-
|
|
6982
|
+
ledgerAmount(amount)
|
|
6638
6983
|
);
|
|
6639
6984
|
ctx.pullSignature = pull.signature;
|
|
6640
6985
|
const outcome = await sendConfirmToTerminal(ctx.rpc, pull, {});
|
|
@@ -6840,7 +7185,7 @@ var AgentRuntime = class {
|
|
|
6840
7185
|
* rather than losing the customer's paid job.
|
|
6841
7186
|
*/
|
|
6842
7187
|
async seedGuarded(jobId, kind, run) {
|
|
6843
|
-
const log = this.
|
|
7188
|
+
const log = this.operatorLog;
|
|
6844
7189
|
const started = Date.now();
|
|
6845
7190
|
try {
|
|
6846
7191
|
const seeded = await run();
|
|
@@ -6987,6 +7332,9 @@ var AgentRuntime = class {
|
|
|
6987
7332
|
return void 0;
|
|
6988
7333
|
}
|
|
6989
7334
|
try {
|
|
7335
|
+
if (await isBlockingNode(filePath)) {
|
|
7336
|
+
return void 0;
|
|
7337
|
+
}
|
|
6990
7338
|
const bytes = await readFile(filePath);
|
|
6991
7339
|
return await this.seedBlossomMember(bytes, recipientPubkey);
|
|
6992
7340
|
} catch {
|
|
@@ -7157,7 +7505,7 @@ var AgentRuntime = class {
|
|
|
7157
7505
|
* polls for on-chain confirmation. Aborts if signal fires.
|
|
7158
7506
|
*/
|
|
7159
7507
|
async collectPayment(job, jobPrice, jobAsset, signal) {
|
|
7160
|
-
const log = this.
|
|
7508
|
+
const log = this.operatorLog;
|
|
7161
7509
|
if (!this.config.solanaAddress) {
|
|
7162
7510
|
throw new Error("Solana address not configured");
|
|
7163
7511
|
}
|
|
@@ -7229,7 +7577,7 @@ var AgentRuntime = class {
|
|
|
7229
7577
|
return;
|
|
7230
7578
|
}
|
|
7231
7579
|
const txSignature = askedSignature ?? verified.txSignature;
|
|
7232
|
-
if (txSignature
|
|
7580
|
+
if (!isUsableSignature(txSignature)) {
|
|
7233
7581
|
paymentRefusal = { consumedByOther: false, sentence: UNNAMED_SETTLEMENT_SENTENCE };
|
|
7234
7582
|
lose({ verified: false }, `${pathLabel}: ${UNNAMED_SETTLEMENT_SENTENCE}`);
|
|
7235
7583
|
return;
|
|
@@ -7398,7 +7746,7 @@ var AgentRuntime = class {
|
|
|
7398
7746
|
(jobId) => this.ledger.getStatus(jobId) === "paid"
|
|
7399
7747
|
);
|
|
7400
7748
|
}
|
|
7401
|
-
const log = this.
|
|
7749
|
+
const log = this.operatorLog;
|
|
7402
7750
|
this.logDeferralSummary(log);
|
|
7403
7751
|
if (pending.length === 0) {
|
|
7404
7752
|
return;
|
|
@@ -8246,7 +8594,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv, network) {
|
|
|
8246
8594
|
}
|
|
8247
8595
|
return skill;
|
|
8248
8596
|
}
|
|
8249
|
-
function
|
|
8597
|
+
function loadSkillsFromDir2(skillsDir, options) {
|
|
8250
8598
|
const skills = [];
|
|
8251
8599
|
let entries;
|
|
8252
8600
|
try {
|
|
@@ -8265,6 +8613,9 @@ function loadSkillsFromDir(skillsDir, options) {
|
|
|
8265
8613
|
}
|
|
8266
8614
|
const skillMdPath = join(entryPath, "SKILL.md");
|
|
8267
8615
|
try {
|
|
8616
|
+
if (isBlockingNodeSync(skillMdPath)) {
|
|
8617
|
+
throw new Error("it is a pipe, socket or device, not a file");
|
|
8618
|
+
}
|
|
8268
8619
|
const content = readFileSync(skillMdPath, "utf-8");
|
|
8269
8620
|
const { frontmatter, systemPrompt } = parseSkillMd(content);
|
|
8270
8621
|
const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
|
|
@@ -8612,6 +8963,9 @@ function startWatchdog(deps) {
|
|
|
8612
8963
|
}
|
|
8613
8964
|
};
|
|
8614
8965
|
}
|
|
8966
|
+
var INDEX_FILE_MODE = 384;
|
|
8967
|
+
var REPAIR_HINT = "every bridged job is refused until that entry is corrected or removed by hand; removing it forgets what was already paid for that job";
|
|
8968
|
+
var STRANDED_TEMP_MIN_AGE_MS2 = 60 * 60 * 1e3;
|
|
8615
8969
|
var X402_JOBS_FILE = ".x402-jobs.json";
|
|
8616
8970
|
var X402_RESULTS_DIR = ".x402-results";
|
|
8617
8971
|
function sanitizeJobId(jobId) {
|
|
@@ -8637,6 +8991,11 @@ var X402JobStore = class {
|
|
|
8637
8991
|
return scheduled;
|
|
8638
8992
|
}
|
|
8639
8993
|
async load() {
|
|
8994
|
+
if (await isBlockingNode(this.jobsPath)) {
|
|
8995
|
+
throw new Error(
|
|
8996
|
+
`Refusing to read ${this.jobsPath}: it is a pipe, socket or device, not a file`
|
|
8997
|
+
);
|
|
8998
|
+
}
|
|
8640
8999
|
let raw;
|
|
8641
9000
|
try {
|
|
8642
9001
|
raw = await readFile(this.jobsPath, "utf-8");
|
|
@@ -8650,16 +9009,73 @@ var X402JobStore = class {
|
|
|
8650
9009
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
8651
9010
|
throw new Error(`x402 store ${this.jobsPath} is not a JSON object`);
|
|
8652
9011
|
}
|
|
9012
|
+
const isCount = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
9013
|
+
for (const [jobId, record] of Object.entries(parsed)) {
|
|
9014
|
+
if (record === null || typeof record !== "object" || Array.isArray(record)) {
|
|
9015
|
+
throw new Error(
|
|
9016
|
+
`x402 store ${this.jobsPath} holds a non-record at ${jobId} - ${REPAIR_HINT}`
|
|
9017
|
+
);
|
|
9018
|
+
}
|
|
9019
|
+
const slot = record;
|
|
9020
|
+
if (!isCount(slot.attempts) || !isCount(slot.created_at) || !isCount(slot.updated_at) || slot.signatures !== void 0 && !isCount(slot.signatures)) {
|
|
9021
|
+
throw new Error(
|
|
9022
|
+
`x402 store ${this.jobsPath} holds an unusable record at ${jobId} - ${REPAIR_HINT}`
|
|
9023
|
+
);
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
8653
9026
|
return parsed;
|
|
8654
9027
|
}
|
|
8655
9028
|
async save(file) {
|
|
8656
|
-
const tempPath = `${this.jobsPath}.tmp`;
|
|
8657
|
-
|
|
8658
|
-
|
|
9029
|
+
const tempPath = `${this.jobsPath}.tmp.${randomBytes(6).toString("hex")}`;
|
|
9030
|
+
try {
|
|
9031
|
+
await writeFile(tempPath, JSON.stringify(file, null, 2), {
|
|
9032
|
+
encoding: "utf-8",
|
|
9033
|
+
mode: INDEX_FILE_MODE
|
|
9034
|
+
});
|
|
9035
|
+
await chmod(tempPath, INDEX_FILE_MODE);
|
|
9036
|
+
await rename(tempPath, this.jobsPath);
|
|
9037
|
+
} catch (error) {
|
|
9038
|
+
try {
|
|
9039
|
+
await rm(tempPath, { force: true });
|
|
9040
|
+
} catch {
|
|
9041
|
+
}
|
|
9042
|
+
throw error;
|
|
9043
|
+
}
|
|
8659
9044
|
}
|
|
8660
9045
|
resultFilePath(jobId) {
|
|
8661
9046
|
return join(this.resultsDir, sanitizeJobId(jobId));
|
|
8662
9047
|
}
|
|
9048
|
+
/** Remove `<result>.tmp.<hex>` leftovers for one job. Best effort. */
|
|
9049
|
+
async sweepStrandedTemporaries(jobId) {
|
|
9050
|
+
const prefix = `${sanitizeJobId(jobId)}.tmp.`;
|
|
9051
|
+
try {
|
|
9052
|
+
const entries = await readdir(this.resultsDir);
|
|
9053
|
+
await Promise.all(
|
|
9054
|
+
entries.filter((entry) => entry.startsWith(prefix)).map((entry) => this.removeIfStale(join(this.resultsDir, entry)))
|
|
9055
|
+
);
|
|
9056
|
+
} catch {
|
|
9057
|
+
}
|
|
9058
|
+
}
|
|
9059
|
+
/**
|
|
9060
|
+
* Remove a temporary only once it is too old to be a live writer's.
|
|
9061
|
+
*
|
|
9062
|
+
* The queue serializes writers inside ONE process, and there is meant to be
|
|
9063
|
+
* one store per agent directory - but `sweepExpired` runs from the driver's
|
|
9064
|
+
* constructor, so a second `elisym start` on the same directory sweeps while
|
|
9065
|
+
* the first is mid-write. Same argument as the ledger's guard: two agents on
|
|
9066
|
+
* one directory is unsupported, and a sweep is no place to make it worse.
|
|
9067
|
+
*/
|
|
9068
|
+
async removeIfStale(path) {
|
|
9069
|
+
try {
|
|
9070
|
+
const info = await stat(path);
|
|
9071
|
+
if (Date.now() - info.mtimeMs < STRANDED_TEMP_MIN_AGE_MS2) {
|
|
9072
|
+
return;
|
|
9073
|
+
}
|
|
9074
|
+
} catch {
|
|
9075
|
+
return;
|
|
9076
|
+
}
|
|
9077
|
+
await rm(path, { force: true }).catch(() => void 0);
|
|
9078
|
+
}
|
|
8663
9079
|
/**
|
|
8664
9080
|
* Atomically claim one paid attempt: increment iff still under BOTH caps
|
|
8665
9081
|
* (durable attempts and the monotonic signature count), and FLUSH before
|
|
@@ -8740,8 +9156,20 @@ var X402JobStore = class {
|
|
|
8740
9156
|
/** Binary result: bytes hit disk BEFORE the record flush (crash-safe ordering). */
|
|
8741
9157
|
async saveFileResult(jobId, mime, bytes) {
|
|
8742
9158
|
const filePath = this.resultFilePath(jobId);
|
|
8743
|
-
await mkdir(this.resultsDir, { recursive: true });
|
|
8744
|
-
await
|
|
9159
|
+
await mkdir(this.resultsDir, { recursive: true, mode: 448 });
|
|
9160
|
+
await chmod(this.resultsDir, 448).catch(() => {
|
|
9161
|
+
});
|
|
9162
|
+
const tempPath = `${filePath}.tmp.${randomBytes(6).toString("hex")}`;
|
|
9163
|
+
try {
|
|
9164
|
+
await writeFile(tempPath, bytes, { mode: 384 });
|
|
9165
|
+
await rename(tempPath, filePath);
|
|
9166
|
+
} catch (error) {
|
|
9167
|
+
try {
|
|
9168
|
+
await rm(tempPath, { force: true });
|
|
9169
|
+
} catch {
|
|
9170
|
+
}
|
|
9171
|
+
throw error;
|
|
9172
|
+
}
|
|
8745
9173
|
await this.runExclusive(async () => {
|
|
8746
9174
|
const file = await this.load();
|
|
8747
9175
|
const now = Date.now();
|
|
@@ -8777,9 +9205,32 @@ var X402JobStore = class {
|
|
|
8777
9205
|
}
|
|
8778
9206
|
return { data: "", outputMime: result.mime, filePath };
|
|
8779
9207
|
}
|
|
9208
|
+
/**
|
|
9209
|
+
* Remove fragments of the INDEX left by a crash between write and rename.
|
|
9210
|
+
*
|
|
9211
|
+
* Keyed by record id like the result sweep cannot be: the index has no job to
|
|
9212
|
+
* expire with, so nothing would ever visit its fragments - the suffix is
|
|
9213
|
+
* random, the name is not `.x402-jobs.json`, and `sweepStrandedTemporaries`
|
|
9214
|
+
* walks the results directory only. What a fragment holds is which upstream
|
|
9215
|
+
* calls this bridge has already paid for.
|
|
9216
|
+
*/
|
|
9217
|
+
async sweepStrandedIndexTemporaries() {
|
|
9218
|
+
const dir = dirname(this.jobsPath);
|
|
9219
|
+
const legacyName = `${basename(this.jobsPath)}.tmp`;
|
|
9220
|
+
const prefix = `${legacyName}.`;
|
|
9221
|
+
try {
|
|
9222
|
+
const entries = await readdir(dir);
|
|
9223
|
+
await Promise.all(
|
|
9224
|
+
entries.filter((entry) => entry === legacyName || entry.startsWith(prefix)).map((entry) => this.removeIfStale(join(dir, entry)))
|
|
9225
|
+
);
|
|
9226
|
+
} catch {
|
|
9227
|
+
}
|
|
9228
|
+
}
|
|
8780
9229
|
/** Drop records (and their result files) older than the cache TTL. */
|
|
8781
9230
|
async sweepExpired(now = Date.now()) {
|
|
8782
9231
|
await this.runExclusive(async () => {
|
|
9232
|
+
await this.sweepStrandedIndexTemporaries();
|
|
9233
|
+
await chmod(this.jobsPath, INDEX_FILE_MODE).catch(() => void 0);
|
|
8783
9234
|
const file = await this.load();
|
|
8784
9235
|
let changed = false;
|
|
8785
9236
|
for (const [jobId, record] of Object.entries(file)) {
|
|
@@ -8790,6 +9241,7 @@ var X402JobStore = class {
|
|
|
8790
9241
|
changed = true;
|
|
8791
9242
|
await rm(this.resultFilePath(jobId), { force: true }).catch(() => {
|
|
8792
9243
|
});
|
|
9244
|
+
await this.sweepStrandedTemporaries(jobId);
|
|
8793
9245
|
}
|
|
8794
9246
|
if (changed) {
|
|
8795
9247
|
await this.save(file);
|
|
@@ -9456,7 +9908,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9456
9908
|
const scriptEnv = buildScriptEnv(loaded.secrets);
|
|
9457
9909
|
const paths = agentPaths(loaded.dir);
|
|
9458
9910
|
const skillsDir = paths.skills;
|
|
9459
|
-
const allSkills =
|
|
9911
|
+
const allSkills = loadSkillsFromDir2(skillsDir, { network: walletNetwork, scriptEnv });
|
|
9460
9912
|
if (allSkills.length === 0) {
|
|
9461
9913
|
console.error(` ! No skills found in ${skillsDir}
|
|
9462
9914
|
`);
|
|
@@ -9479,6 +9931,31 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9479
9931
|
);
|
|
9480
9932
|
process.exit(1);
|
|
9481
9933
|
}
|
|
9934
|
+
if (hasPaid && solanaAddress) {
|
|
9935
|
+
try {
|
|
9936
|
+
const neighbors = await findSharedPayoutNeighbors(
|
|
9937
|
+
cwd,
|
|
9938
|
+
loaded.dir,
|
|
9939
|
+
walletNetwork,
|
|
9940
|
+
solanaAddress
|
|
9941
|
+
);
|
|
9942
|
+
for (const neighbor of neighbors) {
|
|
9943
|
+
console.log(
|
|
9944
|
+
` ! WARNING: agent "${neighbor.name}" (${neighbor.dir}) ` + (neighbor.paid === "unknown" ? `shares this payout address on ${walletNetwork}, and its skills could not be read to tell whether any of them are paid.` : `is paid at the same address on ${walletNetwork}.`) + ` One transaction can then be counted for jobs of both agents, so a customer could be served twice for one transfer. See https://docs.elisym.network/protocol/payments for what the rail does and does not promise.`
|
|
9945
|
+
);
|
|
9946
|
+
}
|
|
9947
|
+
if (neighbors.length > 0) {
|
|
9948
|
+
console.log();
|
|
9949
|
+
}
|
|
9950
|
+
} catch (error) {
|
|
9951
|
+
console.log(
|
|
9952
|
+
` ! Could not check whether another agent is paid at this address (${deleteControlCharacters(
|
|
9953
|
+
error instanceof Error ? error.message : String(error)
|
|
9954
|
+
)}). Starting anyway.`
|
|
9955
|
+
);
|
|
9956
|
+
console.log();
|
|
9957
|
+
}
|
|
9958
|
+
}
|
|
9482
9959
|
const llmSkills = allSkills.filter((skill) => skill.mode === "llm");
|
|
9483
9960
|
const scriptDepSkills = allSkills.filter(
|
|
9484
9961
|
(skill) => skill.mode !== "llm" && skill.llmOverride?.provider && skill.llmOverride?.model
|
|
@@ -9663,6 +10140,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9663
10140
|
const x402Skills = allSkills.filter((skill) => skill.mode === "x402");
|
|
9664
10141
|
let x402InvariantBroken;
|
|
9665
10142
|
if (x402Skills.length > 0) {
|
|
10143
|
+
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
9666
10144
|
const solanaSecretKey = loaded.secrets.solana_secret_key;
|
|
9667
10145
|
if (solanaSecretKey === void 0 || solanaSecretKey.length === 0) {
|
|
9668
10146
|
x402InvariantBroken = "no solana_secret_key in .secrets.json";
|
|
@@ -9769,8 +10247,11 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9769
10247
|
}
|
|
9770
10248
|
}
|
|
9771
10249
|
if (mediaCacheDirty) {
|
|
10250
|
+
await ensureGitignoreHasPrivateStateEntries(dirname(loaded.dir));
|
|
9772
10251
|
await writeMediaCache(loaded.dir, mediaCache);
|
|
9773
10252
|
}
|
|
10253
|
+
await ensureGitignoreHasPrivateStateEntries(dirname(loaded.dir));
|
|
10254
|
+
const ledger = new JobLedger(paths.jobs);
|
|
9774
10255
|
let profilePublished = false;
|
|
9775
10256
|
try {
|
|
9776
10257
|
await client.discovery.publishProfile(
|
|
@@ -9922,6 +10403,16 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9922
10403
|
console.warn(` ! Run \`npx @elisym/cli delegate-key ${loaded.name}\` to enable it.`);
|
|
9923
10404
|
}
|
|
9924
10405
|
}
|
|
10406
|
+
if (delegateSigner !== void 0) {
|
|
10407
|
+
await ensureGitignoreHasDelegationNoncesEntry(dirname(loaded.dir));
|
|
10408
|
+
}
|
|
10409
|
+
const nonceStore = delegateSigner !== void 0 ? new UsedNonceStore(join(loaded.dir, ".delegation-nonces.json")) : void 0;
|
|
10410
|
+
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
10411
|
+
await ensureGitignoreHasPrivateStateEntries(dirname(loaded.dir));
|
|
10412
|
+
const hasContextSkills = registry.all().some((skill) => skill.context === true);
|
|
10413
|
+
if (hasContextSkills) {
|
|
10414
|
+
await ensureGitignoreHasSessionsEntry(dirname(loaded.dir));
|
|
10415
|
+
}
|
|
9925
10416
|
const buildCard = (skill) => buildCapabilityCard(skill, { walletNetwork, solanaAddress, delegatePubkey });
|
|
9926
10417
|
function retiredModelKeyForSkill(skill) {
|
|
9927
10418
|
if (skill.resolvedTriple) {
|
|
@@ -10011,16 +10502,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
10011
10502
|
});
|
|
10012
10503
|
};
|
|
10013
10504
|
const transport = new NostrTransport(client, identity2, [DEFAULT_KIND_OFFSET]);
|
|
10014
|
-
const ledger = new JobLedger(paths.jobs);
|
|
10015
10505
|
const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
|
|
10016
|
-
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
10017
|
-
if (x402Skills.length > 0) {
|
|
10018
|
-
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
10019
|
-
}
|
|
10020
|
-
const hasContextSkills = registry.all().some((skill) => skill.context === true);
|
|
10021
|
-
if (hasContextSkills) {
|
|
10022
|
-
await ensureGitignoreHasSessionsEntry(dirname(loaded.dir));
|
|
10023
|
-
}
|
|
10024
10506
|
const runtimeConfig = {
|
|
10025
10507
|
paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
|
|
10026
10508
|
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
|
@@ -10035,10 +10517,6 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
10035
10517
|
// the payment key) to execute post-work pulls from customer delegations.
|
|
10036
10518
|
delegateSigner
|
|
10037
10519
|
};
|
|
10038
|
-
const nonceStore = delegateSigner !== void 0 ? new UsedNonceStore(join(loaded.dir, ".delegation-nonces.json")) : void 0;
|
|
10039
|
-
if (nonceStore !== void 0) {
|
|
10040
|
-
await ensureGitignoreHasDelegationNoncesEntry(dirname(loaded.dir));
|
|
10041
|
-
}
|
|
10042
10520
|
const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl(walletNetwork));
|
|
10043
10521
|
logger.debug(
|
|
10044
10522
|
{
|
|
@@ -10188,37 +10666,6 @@ function buildScriptEnv(secrets) {
|
|
|
10188
10666
|
}
|
|
10189
10667
|
return scriptEnv;
|
|
10190
10668
|
}
|
|
10191
|
-
var PUBLIC_SOLANA_RPC_HOSTS = /* @__PURE__ */ new Set([
|
|
10192
|
-
"api.devnet.solana.com",
|
|
10193
|
-
"api.mainnet-beta.solana.com",
|
|
10194
|
-
"api.testnet.solana.com"
|
|
10195
|
-
]);
|
|
10196
|
-
function stripRpcSecrets(raw) {
|
|
10197
|
-
try {
|
|
10198
|
-
const parsed = new URL(raw);
|
|
10199
|
-
parsed.username = "";
|
|
10200
|
-
parsed.password = "";
|
|
10201
|
-
if (!PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname)) {
|
|
10202
|
-
return `${parsed.protocol}//${parsed.host}/***`;
|
|
10203
|
-
}
|
|
10204
|
-
const marker = parsed.search.length > 0 ? "?***" : "";
|
|
10205
|
-
parsed.search = "";
|
|
10206
|
-
return `${parsed.toString()}${marker}`;
|
|
10207
|
-
} catch {
|
|
10208
|
-
return "[unparseable RPC URL]";
|
|
10209
|
-
}
|
|
10210
|
-
}
|
|
10211
|
-
function isPublicSolanaRpcUrl(raw) {
|
|
10212
|
-
try {
|
|
10213
|
-
const parsed = new URL(raw);
|
|
10214
|
-
return PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname) && !parsed.username && !parsed.password && !parsed.search && (parsed.pathname === "" || parsed.pathname === "/");
|
|
10215
|
-
} catch {
|
|
10216
|
-
return false;
|
|
10217
|
-
}
|
|
10218
|
-
}
|
|
10219
|
-
function redactRpcUrlsInText(text) {
|
|
10220
|
-
return text.replace(/https?:\/\/[^\s)'"]+/g, (url) => stripRpcSecrets(url));
|
|
10221
|
-
}
|
|
10222
10669
|
async function resolveMediaField(value, agentDir, cache, blossom, identity2, onCacheUpdate) {
|
|
10223
10670
|
if (!value) {
|
|
10224
10671
|
return void 0;
|
|
@@ -10271,11 +10718,23 @@ async function uploadOrReuse(cacheKey, absPath, containRoot, cache, blossom, ide
|
|
|
10271
10718
|
);
|
|
10272
10719
|
return void 0;
|
|
10273
10720
|
}
|
|
10721
|
+
if (!isImagePath(realPath)) {
|
|
10722
|
+
console.warn(
|
|
10723
|
+
` ! Skipping upload of ${basename(absPath)}: not an image (${IMAGE_EXTENSIONS.join(", ")}). Nothing else is published from an agent directory.`
|
|
10724
|
+
);
|
|
10725
|
+
return void 0;
|
|
10726
|
+
}
|
|
10274
10727
|
try {
|
|
10275
10728
|
const cached = await lookupCachedUrl(cache, cacheKey, realPath);
|
|
10276
10729
|
if (cached) {
|
|
10277
10730
|
return cached;
|
|
10278
10731
|
}
|
|
10732
|
+
if (isBlockingNodeSync(realPath)) {
|
|
10733
|
+
console.warn(
|
|
10734
|
+
` ! Skipping ${basename(realPath)}: it is a pipe, socket or device, not a file`
|
|
10735
|
+
);
|
|
10736
|
+
return void 0;
|
|
10737
|
+
}
|
|
10279
10738
|
console.log(` Uploading ${basename(realPath)}...`);
|
|
10280
10739
|
const data = readFileSync(realPath);
|
|
10281
10740
|
const sha256 = createHash("sha256").update(data).digest("hex");
|