@elisym/cli 0.31.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 +862 -189
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
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, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, 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,
|
|
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';
|
|
17
|
-
import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet,
|
|
16
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
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';
|
|
20
20
|
import { Socket } from 'node:net';
|
|
@@ -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) {
|
|
@@ -3804,9 +4007,6 @@ function resolveProviderApiKey(input) {
|
|
|
3804
4007
|
error: `Provider "${provider}" needs an API key (required by skill(s): ${skillList}). Set secrets.llm_api_keys.${provider} via 'npx @elisym/cli profile <agent>' or export ${descriptor.envVar}.`
|
|
3805
4008
|
};
|
|
3806
4009
|
}
|
|
3807
|
-
function sanitizeForTerminal(value) {
|
|
3808
|
-
return value.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
3809
|
-
}
|
|
3810
4010
|
function resolveLevel(options) {
|
|
3811
4011
|
if (options.level) {
|
|
3812
4012
|
return options.level;
|
|
@@ -3845,7 +4045,7 @@ function createLogger(options = {}) {
|
|
|
3845
4045
|
logger = pino(baseOptions, pino.destination(2));
|
|
3846
4046
|
}
|
|
3847
4047
|
function logWithIndent(line) {
|
|
3848
|
-
process.stdout.write(` ${
|
|
4048
|
+
process.stdout.write(` ${deleteControlCharacters(line)}
|
|
3849
4049
|
`);
|
|
3850
4050
|
}
|
|
3851
4051
|
return {
|
|
@@ -3866,6 +4066,43 @@ var MIME_BY_EXT = {
|
|
|
3866
4066
|
function mimeFromPath(path) {
|
|
3867
4067
|
return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
3868
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
|
+
}
|
|
3869
4106
|
var REFERENCE_SCAN_WINDOW = DEFAULTS.VERIFY_SIGNATURE_LIMIT;
|
|
3870
4107
|
var REFERENCE_SCAN_LIST_ATTEMPTS = 3;
|
|
3871
4108
|
var REFERENCE_SCAN_LIST_RETRY_DELAY_MS = 400;
|
|
@@ -4250,7 +4487,7 @@ var PaymentRecovery = class {
|
|
|
4250
4487
|
* An error is never "the customer did not pay" - the caller keeps the job
|
|
4251
4488
|
* recoverable.
|
|
4252
4489
|
*/
|
|
4253
|
-
async listReferenceCandidates(reference, rpc,
|
|
4490
|
+
async listReferenceCandidates(reference, rpc, pastDeadline) {
|
|
4254
4491
|
let lastMessage = "unknown error";
|
|
4255
4492
|
let attempts = 0;
|
|
4256
4493
|
for (let attempt = 0; attempt < REFERENCE_SCAN_LIST_ATTEMPTS; attempt++) {
|
|
@@ -4266,7 +4503,7 @@ var PaymentRecovery = class {
|
|
|
4266
4503
|
};
|
|
4267
4504
|
} catch (e) {
|
|
4268
4505
|
lastMessage = e?.message ?? "unknown error";
|
|
4269
|
-
if (
|
|
4506
|
+
if (pastDeadline()) {
|
|
4270
4507
|
break;
|
|
4271
4508
|
}
|
|
4272
4509
|
if (attempt < REFERENCE_SCAN_LIST_ATTEMPTS - 1) {
|
|
@@ -4306,7 +4543,9 @@ var PaymentRecovery = class {
|
|
|
4306
4543
|
* the SDK default of 10 retries x 3s x a full window - over ten minutes for
|
|
4307
4544
|
* one deferred entry.
|
|
4308
4545
|
*
|
|
4309
|
-
* 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"):
|
|
4310
4549
|
* 1. the listing SUCCEEDED and came back SHORT of the window, so this is the
|
|
4311
4550
|
* reference's whole history rather than a truncated page. A flood that
|
|
4312
4551
|
* fills the window could be hiding the payment behind it, which is
|
|
@@ -4318,26 +4557,40 @@ var PaymentRecovery = class {
|
|
|
4318
4557
|
* evidence of non-payment;
|
|
4319
4558
|
* 3. no candidate was SKIPPED for belonging to another job. A skip means
|
|
4320
4559
|
* something did touch this reference and we chose not to look at it, so
|
|
4321
|
-
* the scan saw less than the whole truth
|
|
4322
|
-
*
|
|
4323
|
-
*
|
|
4324
|
-
*
|
|
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.
|
|
4325
4570
|
*
|
|
4326
4571
|
* What "none" therefore means is narrow and honest: after the failed-on-chain
|
|
4327
4572
|
* transactions are dropped, the reference's entire history is EMPTY - there
|
|
4328
4573
|
* was nothing at all to look at.
|
|
4329
4574
|
*/
|
|
4330
|
-
async verifyByReferenceScan(reference, rpc, jobId,
|
|
4331
|
-
|
|
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);
|
|
4332
4580
|
if ("error" in listed) {
|
|
4333
4581
|
return { outcome: "inconclusive", error: listed.error };
|
|
4334
4582
|
}
|
|
4335
4583
|
let skippedConsumed = false;
|
|
4584
|
+
let skippedUnusable = false;
|
|
4336
4585
|
let unverifiableCandidate;
|
|
4337
4586
|
for (const candidate of listed.signatures) {
|
|
4338
|
-
if (
|
|
4587
|
+
if (pastDeadline()) {
|
|
4339
4588
|
return { outcome: "inconclusive", error: "the reference scan ran out of time" };
|
|
4340
4589
|
}
|
|
4590
|
+
if (!isUsableSignature(candidate)) {
|
|
4591
|
+
skippedUnusable = true;
|
|
4592
|
+
continue;
|
|
4593
|
+
}
|
|
4341
4594
|
const owner = this.ledger.paymentSignatureOwner(candidate);
|
|
4342
4595
|
if (owner !== void 0 && owner !== jobId) {
|
|
4343
4596
|
skippedConsumed = true;
|
|
@@ -4349,6 +4602,9 @@ var PaymentRecovery = class {
|
|
|
4349
4602
|
}
|
|
4350
4603
|
unverifiableCandidate ??= verified.error ?? "no reason given";
|
|
4351
4604
|
}
|
|
4605
|
+
if (pastDeadline()) {
|
|
4606
|
+
return { outcome: "inconclusive", error: "the reference scan ran out of time" };
|
|
4607
|
+
}
|
|
4352
4608
|
if (listed.windowFull) {
|
|
4353
4609
|
return {
|
|
4354
4610
|
outcome: "inconclusive",
|
|
@@ -4361,6 +4617,12 @@ var PaymentRecovery = class {
|
|
|
4361
4617
|
error: "the reference carries a transaction another job already settled, so this scan did not see the whole picture"
|
|
4362
4618
|
};
|
|
4363
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
|
+
}
|
|
4364
4626
|
if (unverifiableCandidate !== void 0) {
|
|
4365
4627
|
return {
|
|
4366
4628
|
outcome: "inconclusive",
|
|
@@ -4408,10 +4670,25 @@ var PaymentRecovery = class {
|
|
|
4408
4670
|
);
|
|
4409
4671
|
return "corrupt-state";
|
|
4410
4672
|
}
|
|
4411
|
-
|
|
4673
|
+
let deadline;
|
|
4412
4674
|
try {
|
|
4413
4675
|
const rpc = createSolanaRpc(getRpcUrl(this.network));
|
|
4414
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
|
+
}
|
|
4415
4692
|
const verify = async (txSignature2, retries) => {
|
|
4416
4693
|
const verification = this.strategy.verifyPayment(rpc, request, protocolConfig, {
|
|
4417
4694
|
txSignature: txSignature2,
|
|
@@ -4443,7 +4720,7 @@ var PaymentRecovery = class {
|
|
|
4443
4720
|
};
|
|
4444
4721
|
const ownSignature = entry.payment_signature;
|
|
4445
4722
|
let txSignature;
|
|
4446
|
-
if (ownSignature
|
|
4723
|
+
if (isUsableSignature(ownSignature) && !pastDeadline()) {
|
|
4447
4724
|
const own = await verify(ownSignature, OWN_SETTLEMENT_VERIFY_RETRIES);
|
|
4448
4725
|
if (own.verified) {
|
|
4449
4726
|
txSignature = ownSignature;
|
|
@@ -4458,11 +4735,11 @@ var PaymentRecovery = class {
|
|
|
4458
4735
|
reference,
|
|
4459
4736
|
rpc,
|
|
4460
4737
|
entry.job_id,
|
|
4461
|
-
|
|
4738
|
+
pastDeadline,
|
|
4462
4739
|
verify
|
|
4463
4740
|
);
|
|
4464
4741
|
if (scan.outcome === "none") {
|
|
4465
|
-
if (ownSignature
|
|
4742
|
+
if (isUsableSignature(ownSignature)) {
|
|
4466
4743
|
log(
|
|
4467
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.`
|
|
4468
4745
|
);
|
|
@@ -4803,14 +5080,24 @@ var SessionStore = class {
|
|
|
4803
5080
|
}
|
|
4804
5081
|
const payload = lines.map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
4805
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
|
+
}
|
|
4806
5087
|
try {
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
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 });
|
|
4811
5096
|
}
|
|
4812
|
-
|
|
4813
|
-
|
|
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;
|
|
4814
5101
|
}
|
|
4815
5102
|
this.globalBytes += Buffer.byteLength(payload);
|
|
4816
5103
|
}
|
|
@@ -4882,6 +5169,9 @@ var SessionStore = class {
|
|
|
4882
5169
|
let raw;
|
|
4883
5170
|
let size;
|
|
4884
5171
|
try {
|
|
5172
|
+
if (isBlockingNodeSync(path)) {
|
|
5173
|
+
return null;
|
|
5174
|
+
}
|
|
4885
5175
|
size = statSync(path).size;
|
|
4886
5176
|
raw = readFileSync(path, "utf-8");
|
|
4887
5177
|
} catch {
|
|
@@ -4913,9 +5203,17 @@ var SessionStore = class {
|
|
|
4913
5203
|
}
|
|
4914
5204
|
if (torn) {
|
|
4915
5205
|
const payload = lines.map((line) => JSON.stringify(line)).join("\n") + (lines.length > 0 ? "\n" : "");
|
|
4916
|
-
const tempPath = `${path}.tmp`;
|
|
4917
|
-
|
|
4918
|
-
|
|
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
|
+
}
|
|
4919
5217
|
this.globalBytes = Math.max(0, this.globalBytes - size + Buffer.byteLength(payload));
|
|
4920
5218
|
}
|
|
4921
5219
|
return lines;
|
|
@@ -4984,10 +5282,18 @@ var SessionStore = class {
|
|
|
4984
5282
|
const payload = rewritten.map((line) => JSON.stringify(line)).join("\n") + "\n";
|
|
4985
5283
|
const path = this.sessionPath(customerId, sessionId);
|
|
4986
5284
|
const oldSize = existsSync(path) ? statSync(path).size : 0;
|
|
4987
|
-
const tempPath = `${path}.tmp`;
|
|
5285
|
+
const tempPath = `${path}.tmp.${randomBytes(6).toString("hex")}`;
|
|
4988
5286
|
mkdirSync(join(this.root, customerId), { recursive: true, mode: DIR_MODE });
|
|
4989
|
-
|
|
4990
|
-
|
|
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
|
+
}
|
|
4991
5297
|
this.globalBytes = Math.max(0, this.globalBytes - oldSize + Buffer.byteLength(payload));
|
|
4992
5298
|
return assembleReplay(rewritten, void 0);
|
|
4993
5299
|
}
|
|
@@ -5124,7 +5430,7 @@ var SessionStore = class {
|
|
|
5124
5430
|
continue;
|
|
5125
5431
|
}
|
|
5126
5432
|
for (const name of names) {
|
|
5127
|
-
if (!name.includes(".corrupt.")) {
|
|
5433
|
+
if (!name.includes(".corrupt.") && !name.includes(".tmp")) {
|
|
5128
5434
|
continue;
|
|
5129
5435
|
}
|
|
5130
5436
|
const path = join(dir, name);
|
|
@@ -5290,6 +5596,15 @@ var X402PermanentError = class extends Error {
|
|
|
5290
5596
|
};
|
|
5291
5597
|
|
|
5292
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
|
+
}
|
|
5293
5608
|
var payment = new SolanaPaymentStrategy();
|
|
5294
5609
|
var LEDGER_GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
5295
5610
|
var SIG_PATH_TIMEOUT_MS = 60 * 1e3;
|
|
@@ -5324,24 +5639,106 @@ function resolveJobAsset(tags, skills) {
|
|
|
5324
5639
|
const skill = skills.route(tags);
|
|
5325
5640
|
return skill?.asset ?? NATIVE_SOL;
|
|
5326
5641
|
}
|
|
5327
|
-
var BILLING_BODY_MARKERS3 = ["credit balance", "billing", "insufficient", "insufficient_quota"];
|
|
5328
5642
|
var SCRIPT_BILLING_INVALID_MARKERS = [
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5643
|
+
// Three questions, one row each, so an edit cannot land half-applied: does
|
|
5644
|
+
// this phrase gate THIS pair at all, does it also take every model on the
|
|
5645
|
+
// operator's key offline (`cascades`), and is it about money rather than
|
|
5646
|
+
// credentials (`billing`, which picks the reason the operator is shown and
|
|
5647
|
+
// the recovery probe reads).
|
|
5648
|
+
//
|
|
5649
|
+
// `cascades` is the expensive one. Gating ONE pair on a false positive costs
|
|
5650
|
+
// the operator a capability until the recovery probe clears it; a cascade
|
|
5651
|
+
// costs them every capability on that key. So it needs a phrase an unrelated
|
|
5652
|
+
// failure does not produce: `insufficient` alone is a Solana builder saying
|
|
5653
|
+
// "insufficient funds for rent" and `billing` alone is a form field, while
|
|
5654
|
+
// nothing prints `invalid x-api-key` or `credit balance` except the provider
|
|
5655
|
+
// whose key it is. The bare status words carry `cascades: false` for the same
|
|
5656
|
+
// reason - `unauthorized` is what any proxy says when it did not like a
|
|
5657
|
+
// request. The LLM path next door demands an HTTP 401/402 before cascading;
|
|
5658
|
+
// this is the script path's version of that bar.
|
|
5659
|
+
{ phrase: "credit balance", cascades: true, billing: true },
|
|
5660
|
+
{ phrase: "billing", cascades: false, billing: true },
|
|
5661
|
+
{ phrase: "insufficient", cascades: false, billing: true },
|
|
5662
|
+
{ phrase: "insufficient_quota", cascades: true, billing: true },
|
|
5663
|
+
// Gates this pair, never cascades: `x-api-key` is the name of a REQUEST
|
|
5664
|
+
// HEADER, which `curl -v` and any client that prints its own headers emit on a
|
|
5665
|
+
// perfectly ordinary failure. Taking every model on the key offline for that
|
|
5666
|
+
// is the expensive direction; `invalid x-api-key` below is the provider
|
|
5667
|
+
// actually rejecting it.
|
|
5668
|
+
{ phrase: "x-api-key", cascades: false, billing: false },
|
|
5669
|
+
// The provider REJECTING the key, which no request header says.
|
|
5670
|
+
{ phrase: "invalid x-api-key", cascades: true, billing: false },
|
|
5671
|
+
{ phrase: "invalid api key", cascades: true, billing: false },
|
|
5672
|
+
{ phrase: "invalid_api_key", cascades: true, billing: false },
|
|
5673
|
+
{ phrase: "authentication_error", cascades: true, billing: false },
|
|
5674
|
+
{ phrase: "unauthorized", cascades: false, billing: false },
|
|
5675
|
+
{ phrase: "unauthenticated", cascades: false, billing: false }
|
|
5339
5676
|
];
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5677
|
+
var BILLING_BODY_MARKERS3 = SCRIPT_BILLING_INVALID_MARKERS.filter((marker) => marker.billing).map(
|
|
5678
|
+
(marker) => marker.phrase
|
|
5679
|
+
);
|
|
5680
|
+
function classifyScriptSignal(message) {
|
|
5681
|
+
let looksBillingOrInvalid = false;
|
|
5682
|
+
let billing = false;
|
|
5683
|
+
let cascade = false;
|
|
5684
|
+
let signalAt = -1;
|
|
5685
|
+
for (const match of message.matchAll(ALL_MARKERS_RE)) {
|
|
5686
|
+
const marker = MARKER_BY_PHRASE.get(match[0].toLowerCase());
|
|
5687
|
+
looksBillingOrInvalid = true;
|
|
5688
|
+
billing = billing || marker?.billing === true;
|
|
5689
|
+
cascade = cascade || marker?.cascades === true;
|
|
5690
|
+
if (signalAt === -1) {
|
|
5691
|
+
signalAt = match.index;
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
return { looksBillingOrInvalid, reason: billing ? "billing" : "invalid", cascade, signalAt };
|
|
5695
|
+
}
|
|
5696
|
+
var HOST_DISK_CODES = /* @__PURE__ */ new Set(["ENOSPC", "EROFS", "EDQUOT", "EMFILE", "ENFILE", "EIO"]);
|
|
5697
|
+
function asHostScratchFailure(error) {
|
|
5698
|
+
const code = error?.code;
|
|
5699
|
+
if (code === void 0 || !HOST_DISK_CODES.has(code)) {
|
|
5700
|
+
return void 0;
|
|
5701
|
+
}
|
|
5702
|
+
return new HostScratchError(error instanceof Error ? error.message : String(error));
|
|
5703
|
+
}
|
|
5704
|
+
var HEALTH_REASON_CHARS = 500;
|
|
5705
|
+
var SIGNAL_LEAD_UNITS = 80;
|
|
5706
|
+
function markerPattern(phrases, flags) {
|
|
5707
|
+
const longestFirst = [...phrases].sort((left, right) => right.length - left.length);
|
|
5708
|
+
return new RegExp(
|
|
5709
|
+
longestFirst.map((phrase) => phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
|
|
5710
|
+
flags
|
|
5711
|
+
);
|
|
5712
|
+
}
|
|
5713
|
+
var ALL_MARKERS_RE = markerPattern(
|
|
5714
|
+
SCRIPT_BILLING_INVALID_MARKERS.map((marker) => marker.phrase),
|
|
5715
|
+
"gi"
|
|
5716
|
+
);
|
|
5717
|
+
var MARKER_BY_PHRASE = new Map(
|
|
5718
|
+
SCRIPT_BILLING_INVALID_MARKERS.map((marker) => [marker.phrase, marker])
|
|
5719
|
+
);
|
|
5720
|
+
function signalIndex(text) {
|
|
5721
|
+
ALL_MARKERS_RE.lastIndex = 0;
|
|
5722
|
+
const found = ALL_MARKERS_RE.exec(text)?.index ?? -1;
|
|
5723
|
+
ALL_MARKERS_RE.lastIndex = 0;
|
|
5724
|
+
return found;
|
|
5725
|
+
}
|
|
5726
|
+
function operatorReason(diagnostic, known, budget = HEALTH_REASON_CHARS) {
|
|
5727
|
+
if (startsWithRefusalHint(diagnostic)) {
|
|
5728
|
+
return excerptUntrusted(diagnostic, budget);
|
|
5729
|
+
}
|
|
5730
|
+
const signalAt = known ?? signalIndex(diagnostic);
|
|
5731
|
+
if (signalAt === -1) {
|
|
5732
|
+
return excerptUntrustedTail(diagnostic, budget);
|
|
5733
|
+
}
|
|
5734
|
+
const from = Math.max(0, signalAt - SIGNAL_LEAD_UNITS);
|
|
5735
|
+
if (from === 0) {
|
|
5736
|
+
return excerptUntrusted(diagnostic, budget);
|
|
5737
|
+
}
|
|
5738
|
+
return `\u2026${excerptUntrusted(diagnostic.slice(from, from + budget * 8), budget - 1)}`;
|
|
5343
5739
|
}
|
|
5344
5740
|
var AGENT_UNAVAILABLE_MESSAGE = "Agent temporarily unavailable";
|
|
5741
|
+
var OPERATOR_EXCERPT_CHARS = 1500;
|
|
5345
5742
|
var AgentUnavailableError = class extends Error {
|
|
5346
5743
|
constructor() {
|
|
5347
5744
|
super(AGENT_UNAVAILABLE_MESSAGE);
|
|
@@ -5395,16 +5792,49 @@ function customerSafeMessage(error) {
|
|
|
5395
5792
|
if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError || error instanceof SeedFailedError) {
|
|
5396
5793
|
return error.message;
|
|
5397
5794
|
}
|
|
5398
|
-
if (error
|
|
5399
|
-
return error.message
|
|
5795
|
+
if (isScriptRefusalError(error)) {
|
|
5796
|
+
return `${PROVIDER_REFUSED_PREFIX}${error.message}`;
|
|
5400
5797
|
}
|
|
5401
|
-
if (error
|
|
5798
|
+
if (isScriptExecutionError(error)) {
|
|
5799
|
+
return PROVIDER_FAILED_MESSAGE;
|
|
5800
|
+
}
|
|
5801
|
+
if (isScriptBillingExhaustedError(error) || isHostScratchError(error) || error instanceof X402TransientError) {
|
|
5402
5802
|
return AGENT_UNAVAILABLE_MESSAGE;
|
|
5403
5803
|
}
|
|
5404
5804
|
if (error instanceof Error && CUSTOMER_SAFE_MESSAGE_PREFIXES.some((prefix) => error.message.startsWith(prefix))) {
|
|
5405
5805
|
return error.message;
|
|
5406
5806
|
}
|
|
5407
|
-
return
|
|
5807
|
+
return PROVIDER_FAILED_MESSAGE;
|
|
5808
|
+
}
|
|
5809
|
+
function needsScratchSpace(mode, hasAttachment) {
|
|
5810
|
+
if (hasAttachment) {
|
|
5811
|
+
return true;
|
|
5812
|
+
}
|
|
5813
|
+
return mode !== void 0 && mode !== "llm" && mode !== "static-file" && mode !== "static-script";
|
|
5814
|
+
}
|
|
5815
|
+
function describeForOperator(error) {
|
|
5816
|
+
if (isHostScratchError(error)) {
|
|
5817
|
+
return `${error.message}: ${excerptUntrusted(error.detail, OPERATOR_EXCERPT_CHARS)}`;
|
|
5818
|
+
}
|
|
5819
|
+
if (isScriptBillingExhaustedError(error)) {
|
|
5820
|
+
return `script signalled billing exhausted: ${excerptUntrustedTail(scriptOutput(error), OPERATOR_EXCERPT_CHARS)}`;
|
|
5821
|
+
}
|
|
5822
|
+
if (isScriptRefusalError(error)) {
|
|
5823
|
+
return error.stderr === "" ? `refused: ${error.message}` : `refused: ${error.message} (stderr: ${error.stderr})`;
|
|
5824
|
+
}
|
|
5825
|
+
if (isScriptExecutionError(error)) {
|
|
5826
|
+
return `${error.message}: ${excerptUntrustedTail(error.detail, OPERATOR_EXCERPT_CHARS)}`;
|
|
5827
|
+
}
|
|
5828
|
+
if (typeof error === "string") {
|
|
5829
|
+
return excerptUntrusted(error, OPERATOR_EXCERPT_CHARS);
|
|
5830
|
+
}
|
|
5831
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
5832
|
+
const message = error.message;
|
|
5833
|
+
if (typeof message === "string" && message !== "") {
|
|
5834
|
+
return excerptUntrusted(message, OPERATOR_EXCERPT_CHARS);
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
return "Unknown error";
|
|
5408
5838
|
}
|
|
5409
5839
|
function parseRawEventDelegated(rawEventJson) {
|
|
5410
5840
|
if (rawEventJson === void 0) {
|
|
@@ -5691,12 +6121,51 @@ var AgentRuntime = class {
|
|
|
5691
6121
|
}
|
|
5692
6122
|
return ` (cascading to ${siblings} other model(s) for ${provider} sharing the same API key)`;
|
|
5693
6123
|
}
|
|
6124
|
+
/**
|
|
6125
|
+
* Whether this agent can still make itself a scratch directory.
|
|
6126
|
+
*
|
|
6127
|
+
* A `HostScratchError` says the last job could not run because the temp
|
|
6128
|
+
* directory refused it. That is not the operator's API key, so it must not
|
|
6129
|
+
* gate the health pair - but SOMETHING has to stop the next customer paying
|
|
6130
|
+
* into an agent that will fail them the same way, which is what the health
|
|
6131
|
+
* gate used to do by accident. Probed before payment, and only once such a
|
|
6132
|
+
* failure has been seen - a working host pays nothing for this, and a broken
|
|
6133
|
+
* one is asked again on every job rather than after a timer: an agent whose
|
|
6134
|
+
* jobs arrive minutes apart would otherwise let every customer through.
|
|
6135
|
+
*/
|
|
6136
|
+
scratchFailed = false;
|
|
6137
|
+
async scratchSpaceUsable() {
|
|
6138
|
+
if (!this.scratchFailed) {
|
|
6139
|
+
return true;
|
|
6140
|
+
}
|
|
6141
|
+
const probe = await mkdtemp(join(tmpdir(), "elisym-probe-")).catch(() => null);
|
|
6142
|
+
if (probe === null) {
|
|
6143
|
+
return false;
|
|
6144
|
+
}
|
|
6145
|
+
const wrote = await writeFile(join(probe, "probe"), "x").then(() => true).catch(() => false);
|
|
6146
|
+
await rm(probe, { recursive: true, force: true }).catch(() => {
|
|
6147
|
+
});
|
|
6148
|
+
if (!wrote) {
|
|
6149
|
+
return false;
|
|
6150
|
+
}
|
|
6151
|
+
this.scratchFailed = false;
|
|
6152
|
+
return true;
|
|
6153
|
+
}
|
|
5694
6154
|
markHealthFromExecuteError(skill, err, log, jobId) {
|
|
5695
6155
|
if (!this.healthMonitor) {
|
|
5696
6156
|
return false;
|
|
5697
6157
|
}
|
|
5698
6158
|
const tag = `[${jobId.slice(0, 8)}]`;
|
|
5699
|
-
if (err
|
|
6159
|
+
if (isHostScratchError(err)) {
|
|
6160
|
+
log(
|
|
6161
|
+
`${tag} Skill "${skill.name}" could not be given scratch space by THIS AGENT: ${excerptUntrusted(err.detail, 300)} Health state unchanged; the job stays paid for recovery.`
|
|
6162
|
+
);
|
|
6163
|
+
return false;
|
|
6164
|
+
}
|
|
6165
|
+
if (isScriptRefusalError(err)) {
|
|
6166
|
+
return false;
|
|
6167
|
+
}
|
|
6168
|
+
if (isScriptBillingExhaustedError(err)) {
|
|
5700
6169
|
const provider = skill.llmOverride?.provider;
|
|
5701
6170
|
const model = skill.llmOverride?.model;
|
|
5702
6171
|
if (!provider || !model) {
|
|
@@ -5708,7 +6177,12 @@ var AgentRuntime = class {
|
|
|
5708
6177
|
log(
|
|
5709
6178
|
`${tag} Script signaled billing-exhausted (exit ${err.exitCode}). Marking ${provider}/${model} unhealthy${this.cascadeSuffix(provider, model)}; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
5710
6179
|
);
|
|
5711
|
-
this.healthMonitor.markUnhealthyFromJob(
|
|
6180
|
+
this.healthMonitor.markUnhealthyFromJob(
|
|
6181
|
+
provider,
|
|
6182
|
+
model,
|
|
6183
|
+
"billing",
|
|
6184
|
+
excerptUntrustedTail(scriptOutput(err), 200)
|
|
6185
|
+
);
|
|
5712
6186
|
return true;
|
|
5713
6187
|
}
|
|
5714
6188
|
if (skill.mode === "llm" && skill.resolvedTriple) {
|
|
@@ -5718,7 +6192,7 @@ var AgentRuntime = class {
|
|
|
5718
6192
|
return false;
|
|
5719
6193
|
}
|
|
5720
6194
|
const status = Number(match[1]);
|
|
5721
|
-
const body = (match[2] ?? ""
|
|
6195
|
+
const body = excerptUntrusted(match[2] ?? "", 200);
|
|
5722
6196
|
const isBillingStatus = status === 402;
|
|
5723
6197
|
const isAuthStatus = status === 401 || status === 403;
|
|
5724
6198
|
const isBilling400 = status === 400 && bodyLooksLikeBilling3(body);
|
|
@@ -5737,33 +6211,43 @@ var AgentRuntime = class {
|
|
|
5737
6211
|
}
|
|
5738
6212
|
if (skill.mode !== "llm") {
|
|
5739
6213
|
let message;
|
|
5740
|
-
|
|
5741
|
-
|
|
6214
|
+
let scanned;
|
|
6215
|
+
let diagnostic;
|
|
6216
|
+
if (isScriptExecutionError(err)) {
|
|
6217
|
+
message = err.stderr ?? err.detail;
|
|
6218
|
+
diagnostic = startsWithRefusalHint(err.detail) || message.trim() === "" ? err.detail : message;
|
|
6219
|
+
scanned = err.stderr === void 0 ? "the skill error" : "stderr";
|
|
5742
6220
|
} else if (err instanceof Error) {
|
|
5743
6221
|
message = err.message;
|
|
6222
|
+
diagnostic = message;
|
|
6223
|
+
scanned = "the skill error";
|
|
5744
6224
|
} else {
|
|
5745
6225
|
message = String(err);
|
|
6226
|
+
diagnostic = message;
|
|
6227
|
+
scanned = "the skill error";
|
|
5746
6228
|
}
|
|
5747
6229
|
const provider = skill.llmOverride?.provider;
|
|
5748
6230
|
const model = skill.llmOverride?.model;
|
|
5749
6231
|
if (!provider || !model) {
|
|
5750
6232
|
log(
|
|
5751
|
-
`${tag} Script "${skill.name}" failed ("${
|
|
6233
|
+
`${tag} Script "${skill.name}" failed ("${operatorReason(diagnostic)}") but did not declare provider/model in SKILL.md - cannot gate future jobs.`
|
|
5752
6234
|
);
|
|
5753
6235
|
return false;
|
|
5754
6236
|
}
|
|
5755
|
-
const
|
|
5756
|
-
const looksBillingOrInvalid = scriptMessageLooksLikeBillingOrInvalid(message);
|
|
5757
|
-
const reason = looksBillingOrInvalid && (lower.includes("credit balance") || lower.includes("billing") || lower.includes("insufficient")) ? "billing" : "invalid";
|
|
5758
|
-
const cascade = looksBillingOrInvalid;
|
|
6237
|
+
const { looksBillingOrInvalid, reason, cascade, signalAt } = classifyScriptSignal(message);
|
|
5759
6238
|
const cascadeNote = cascade ? this.cascadeSuffix(provider, model) : " (no cascade)";
|
|
5760
|
-
const signalNote = looksBillingOrInvalid ? `${reason} signal in
|
|
6239
|
+
const signalNote = looksBillingOrInvalid ? `${reason} signal in ${scanned}` : `generic exit (no billing/invalid markers, classified as ${reason}, skill-local)`;
|
|
5761
6240
|
log(
|
|
5762
6241
|
`${tag} Script failure (${signalNote}). Marking ${provider}/${model} unhealthy${cascadeNote}; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
5763
6242
|
);
|
|
5764
|
-
this.healthMonitor.markUnhealthyFromJob(
|
|
5765
|
-
|
|
5766
|
-
|
|
6243
|
+
this.healthMonitor.markUnhealthyFromJob(
|
|
6244
|
+
provider,
|
|
6245
|
+
model,
|
|
6246
|
+
reason,
|
|
6247
|
+
// The index only when the quote is of the same string the scan read.
|
|
6248
|
+
operatorReason(diagnostic, diagnostic === message ? signalAt : void 0),
|
|
6249
|
+
{ cascade }
|
|
6250
|
+
);
|
|
5767
6251
|
return true;
|
|
5768
6252
|
}
|
|
5769
6253
|
return false;
|
|
@@ -5777,8 +6261,31 @@ var AgentRuntime = class {
|
|
|
5777
6261
|
});
|
|
5778
6262
|
return { feeBps: config.feeBps, treasury: config.treasury };
|
|
5779
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
|
+
}
|
|
5780
6287
|
async run() {
|
|
5781
|
-
const log = this.
|
|
6288
|
+
const log = this.operatorLog;
|
|
5782
6289
|
this.ledger.pruneOldEntries(LEDGER_RETENTION_MS);
|
|
5783
6290
|
this.sessionStore?.init();
|
|
5784
6291
|
this.reconcileUsedNonces();
|
|
@@ -5878,8 +6385,8 @@ var AgentRuntime = class {
|
|
|
5878
6385
|
if (jobSession) {
|
|
5879
6386
|
jobSession.store.admitLive(jobSession.customerId, jobSession.sessionId);
|
|
5880
6387
|
}
|
|
5881
|
-
this.limit(() => this.processJob(job)).catch((
|
|
5882
|
-
this.callbacks.onJobError?.(job.jobId,
|
|
6388
|
+
this.limit(() => this.processJob(job)).catch((err) => {
|
|
6389
|
+
this.callbacks.onJobError?.(job.jobId, describeForOperator(err));
|
|
5883
6390
|
}).finally(() => {
|
|
5884
6391
|
this.inFlight.delete(job.jobId);
|
|
5885
6392
|
this.pending--;
|
|
@@ -5928,7 +6435,7 @@ var AgentRuntime = class {
|
|
|
5928
6435
|
this.callbacks.onStop?.();
|
|
5929
6436
|
} catch (error) {
|
|
5930
6437
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5931
|
-
|
|
6438
|
+
this.operatorLog(`onStop error: ${msg}`);
|
|
5932
6439
|
}
|
|
5933
6440
|
this.abortController.abort();
|
|
5934
6441
|
for (const controller of this.jobAbortControllers) {
|
|
@@ -5964,18 +6471,31 @@ var AgentRuntime = class {
|
|
|
5964
6471
|
this.jobAbortControllers.add(jobAbort);
|
|
5965
6472
|
try {
|
|
5966
6473
|
await this.executeJob(job, jobAbort.signal);
|
|
5967
|
-
} catch (
|
|
5968
|
-
const
|
|
5969
|
-
log
|
|
6474
|
+
} catch (raw) {
|
|
6475
|
+
const e = asHostScratchFailure(raw) ?? raw;
|
|
6476
|
+
const log = this.operatorLog;
|
|
6477
|
+
const operatorMessage = describeForOperator(e);
|
|
6478
|
+
log(`[${job.jobId.slice(0, 8)}] Error: ${operatorMessage}`);
|
|
6479
|
+
if (isHostScratchError(e)) {
|
|
6480
|
+
this.scratchFailed = true;
|
|
6481
|
+
}
|
|
5970
6482
|
const currentStatus = this.ledger.getStatus(job.jobId);
|
|
5971
|
-
const keepPaidForRecovery = (e instanceof AgentUnavailableError ||
|
|
6483
|
+
const keepPaidForRecovery = (e instanceof AgentUnavailableError || // The exit-42 contract: the key is out of credits, not the job out of
|
|
6484
|
+
// sense, so the job waits for the operator to top up. Reached when the
|
|
6485
|
+
// gate did NOT flip - an agent with no health monitor, or a skill that
|
|
6486
|
+
// declares no pair - where the customer is told "temporarily
|
|
6487
|
+
// unavailable" and that has to stay true of their money.
|
|
6488
|
+
isScriptBillingExhaustedError(e) || // The agent's own disk, not the job: a tmpdir that is full or
|
|
6489
|
+
// read-only now may not be in five minutes, and the customer has
|
|
6490
|
+
// already paid. Terminating here would keep their money for a failure
|
|
6491
|
+
// that was never about their request.
|
|
6492
|
+
isHostScratchError(e) || e instanceof SeedFailedError || e instanceof PaymentTimeoutError || e instanceof X402TransientError || e instanceof ExecutionBudgetExceededError && this.skills.route(job.tags)?.mode === "x402") && currentStatus === "paid";
|
|
5972
6493
|
if (currentStatus !== "executed" && !keepPaidForRecovery) {
|
|
5973
6494
|
this.ledger.markFailed(job.jobId);
|
|
5974
6495
|
}
|
|
5975
6496
|
if (keepPaidForRecovery) {
|
|
5976
6497
|
log(`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will retry (24h cutoff).`);
|
|
5977
6498
|
}
|
|
5978
|
-
const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
|
|
5979
6499
|
this.callbacks.onJobError?.(job.jobId, operatorMessage);
|
|
5980
6500
|
const safeMessage = customerSafeMessage(e);
|
|
5981
6501
|
await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
|
|
@@ -5986,7 +6506,7 @@ var AgentRuntime = class {
|
|
|
5986
6506
|
}
|
|
5987
6507
|
/** Core job processing logic - payment, skill execution, result delivery. */
|
|
5988
6508
|
async executeJob(job, signal) {
|
|
5989
|
-
const log = this.
|
|
6509
|
+
const log = this.operatorLog;
|
|
5990
6510
|
const inputBytes = utf8ByteLength(job.input);
|
|
5991
6511
|
if (inputBytes > LIMITS.MAX_INPUT_LENGTH) {
|
|
5992
6512
|
throw new Error(`Input too long: ${inputBytes} bytes (max ${LIMITS.MAX_INPUT_LENGTH})`);
|
|
@@ -6007,13 +6527,25 @@ var AgentRuntime = class {
|
|
|
6007
6527
|
return;
|
|
6008
6528
|
}
|
|
6009
6529
|
}
|
|
6530
|
+
if (needsScratchSpace(matched?.mode, job.attachment !== void 0) && !await this.scratchSpaceUsable()) {
|
|
6531
|
+
log(
|
|
6532
|
+
`[${job.jobId.slice(0, 8)}] Refusing job before payment: this agent cannot create scratch space (check the temp directory).`
|
|
6533
|
+
);
|
|
6534
|
+
await this.transport.sendFeedback(job, { type: "error", message: AGENT_UNAVAILABLE_MESSAGE }).catch(() => {
|
|
6535
|
+
});
|
|
6536
|
+
return;
|
|
6537
|
+
}
|
|
6010
6538
|
if (matched?.mode === "x402" && job.attachment !== void 0) {
|
|
6011
6539
|
const method = matched.x402?.method ?? "POST";
|
|
6012
6540
|
const maxInputBytes = matched.x402?.maxInputBytes ?? 0;
|
|
6013
6541
|
const attachmentOk = method === "POST" && job.attachment.mime.startsWith("text/") && job.attachment.size <= maxInputBytes;
|
|
6014
6542
|
if (!attachmentOk) {
|
|
6015
6543
|
log(
|
|
6016
|
-
`
|
|
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})`
|
|
6017
6549
|
);
|
|
6018
6550
|
await this.transport.sendFeedback(job, {
|
|
6019
6551
|
type: "error",
|
|
@@ -6066,7 +6598,7 @@ var AgentRuntime = class {
|
|
|
6066
6598
|
if (delegatedPull === void 0) {
|
|
6067
6599
|
return;
|
|
6068
6600
|
}
|
|
6069
|
-
netAmount =
|
|
6601
|
+
netAmount = ledgerAmount(delegatedPull.priceSubunits);
|
|
6070
6602
|
} else if (jobPrice > 0) {
|
|
6071
6603
|
const result = await this.collectPayment(job, jobPrice, jobAsset, signal);
|
|
6072
6604
|
netAmount = result.netAmount;
|
|
@@ -6190,7 +6722,7 @@ var AgentRuntime = class {
|
|
|
6190
6722
|
log
|
|
6191
6723
|
);
|
|
6192
6724
|
const outcome = await this.executeDelegatedPull(job, delegatedPull, log);
|
|
6193
|
-
netAmount =
|
|
6725
|
+
netAmount = ledgerAmount(delegatedPull.chargedSubunits ?? delegatedPull.priceSubunits);
|
|
6194
6726
|
if (outcome === "dead") {
|
|
6195
6727
|
this.ledger.markFailed(job.jobId);
|
|
6196
6728
|
await this.transport.sendFeedback(job, {
|
|
@@ -6447,7 +6979,7 @@ var AgentRuntime = class {
|
|
|
6447
6979
|
job.jobId,
|
|
6448
6980
|
pull.signature,
|
|
6449
6981
|
Number(pull.lastValidBlockHeight),
|
|
6450
|
-
|
|
6982
|
+
ledgerAmount(amount)
|
|
6451
6983
|
);
|
|
6452
6984
|
ctx.pullSignature = pull.signature;
|
|
6453
6985
|
const outcome = await sendConfirmToTerminal(ctx.rpc, pull, {});
|
|
@@ -6653,7 +7185,7 @@ var AgentRuntime = class {
|
|
|
6653
7185
|
* rather than losing the customer's paid job.
|
|
6654
7186
|
*/
|
|
6655
7187
|
async seedGuarded(jobId, kind, run) {
|
|
6656
|
-
const log = this.
|
|
7188
|
+
const log = this.operatorLog;
|
|
6657
7189
|
const started = Date.now();
|
|
6658
7190
|
try {
|
|
6659
7191
|
const seeded = await run();
|
|
@@ -6800,6 +7332,9 @@ var AgentRuntime = class {
|
|
|
6800
7332
|
return void 0;
|
|
6801
7333
|
}
|
|
6802
7334
|
try {
|
|
7335
|
+
if (await isBlockingNode(filePath)) {
|
|
7336
|
+
return void 0;
|
|
7337
|
+
}
|
|
6803
7338
|
const bytes = await readFile(filePath);
|
|
6804
7339
|
return await this.seedBlossomMember(bytes, recipientPubkey);
|
|
6805
7340
|
} catch {
|
|
@@ -6893,7 +7428,9 @@ var AgentRuntime = class {
|
|
|
6893
7428
|
});
|
|
6894
7429
|
return this.materializeBytesInput(bytes, attachment.mime);
|
|
6895
7430
|
}
|
|
6896
|
-
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"))
|
|
7431
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-")).catch((err) => {
|
|
7432
|
+
throw new HostScratchError(err instanceof Error ? err.message : String(err));
|
|
7433
|
+
});
|
|
6897
7434
|
const filePath = join(dir, "input");
|
|
6898
7435
|
try {
|
|
6899
7436
|
await this.irohTransport.fetchToPath(irohMember.ticket, filePath, {
|
|
@@ -6945,14 +7482,16 @@ var AgentRuntime = class {
|
|
|
6945
7482
|
return { inlineText: Buffer.from(bytes).toString("utf8"), cleanup: async () => {
|
|
6946
7483
|
} };
|
|
6947
7484
|
}
|
|
6948
|
-
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"))
|
|
7485
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-")).catch((err) => {
|
|
7486
|
+
throw new HostScratchError(err instanceof Error ? err.message : String(err));
|
|
7487
|
+
});
|
|
6949
7488
|
const filePath = join(dir, "input");
|
|
6950
7489
|
try {
|
|
6951
7490
|
await writeFile(filePath, bytes);
|
|
6952
7491
|
} catch (error) {
|
|
6953
7492
|
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
6954
7493
|
});
|
|
6955
|
-
throw error;
|
|
7494
|
+
throw asHostScratchFailure(error) ?? error;
|
|
6956
7495
|
}
|
|
6957
7496
|
return {
|
|
6958
7497
|
filePath,
|
|
@@ -6966,7 +7505,7 @@ var AgentRuntime = class {
|
|
|
6966
7505
|
* polls for on-chain confirmation. Aborts if signal fires.
|
|
6967
7506
|
*/
|
|
6968
7507
|
async collectPayment(job, jobPrice, jobAsset, signal) {
|
|
6969
|
-
const log = this.
|
|
7508
|
+
const log = this.operatorLog;
|
|
6970
7509
|
if (!this.config.solanaAddress) {
|
|
6971
7510
|
throw new Error("Solana address not configured");
|
|
6972
7511
|
}
|
|
@@ -7038,7 +7577,7 @@ var AgentRuntime = class {
|
|
|
7038
7577
|
return;
|
|
7039
7578
|
}
|
|
7040
7579
|
const txSignature = askedSignature ?? verified.txSignature;
|
|
7041
|
-
if (txSignature
|
|
7580
|
+
if (!isUsableSignature(txSignature)) {
|
|
7042
7581
|
paymentRefusal = { consumedByOther: false, sentence: UNNAMED_SETTLEMENT_SENTENCE };
|
|
7043
7582
|
lose({ verified: false }, `${pathLabel}: ${UNNAMED_SETTLEMENT_SENTENCE}`);
|
|
7044
7583
|
return;
|
|
@@ -7207,7 +7746,7 @@ var AgentRuntime = class {
|
|
|
7207
7746
|
(jobId) => this.ledger.getStatus(jobId) === "paid"
|
|
7208
7747
|
);
|
|
7209
7748
|
}
|
|
7210
|
-
const log = this.
|
|
7749
|
+
const log = this.operatorLog;
|
|
7211
7750
|
this.logDeferralSummary(log);
|
|
7212
7751
|
if (pending.length === 0) {
|
|
7213
7752
|
return;
|
|
@@ -7287,12 +7826,13 @@ var AgentRuntime = class {
|
|
|
7287
7826
|
this.limit(async () => {
|
|
7288
7827
|
try {
|
|
7289
7828
|
await this.recoverSingleJob(entry, log);
|
|
7290
|
-
} catch (
|
|
7291
|
-
log(`[${entry.job_id.slice(0, 8)}] Recovery: failed: ${
|
|
7829
|
+
} catch (err) {
|
|
7830
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: failed: ${describeForOperator(err)}`);
|
|
7292
7831
|
} finally {
|
|
7293
7832
|
this.inFlight.delete(entry.job_id);
|
|
7294
7833
|
this.pending--;
|
|
7295
7834
|
}
|
|
7835
|
+
}).catch(() => {
|
|
7296
7836
|
});
|
|
7297
7837
|
}
|
|
7298
7838
|
}
|
|
@@ -7398,7 +7938,7 @@ var AgentRuntime = class {
|
|
|
7398
7938
|
try {
|
|
7399
7939
|
await this.healthMonitor.assertReady(healthPair.provider, healthPair.model);
|
|
7400
7940
|
} catch (err) {
|
|
7401
|
-
if (err
|
|
7941
|
+
if (isLlmHealthError(err)) {
|
|
7402
7942
|
log(
|
|
7403
7943
|
`[${entry.job_id.slice(0, 8)}] Recovery: pair ${healthPair.provider}/${healthPair.model} still unhealthy (${err.reason}); waiting for recovery probe.`
|
|
7404
7944
|
);
|
|
@@ -7442,7 +7982,15 @@ var AgentRuntime = class {
|
|
|
7442
7982
|
entry.customer_id,
|
|
7443
7983
|
recoveryAbort.signal
|
|
7444
7984
|
);
|
|
7445
|
-
} catch {
|
|
7985
|
+
} catch (raw) {
|
|
7986
|
+
const err = asHostScratchFailure(raw) ?? raw;
|
|
7987
|
+
if (isHostScratchError(err)) {
|
|
7988
|
+
this.scratchFailed = true;
|
|
7989
|
+
log(
|
|
7990
|
+
`[${entry.job_id.slice(0, 8)}] Recovery: no scratch space for the input file; leaving the job paid.`
|
|
7991
|
+
);
|
|
7992
|
+
return;
|
|
7993
|
+
}
|
|
7446
7994
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
|
|
7447
7995
|
await this.failRecoveredJob(entry, fakeJob, RECOVERY_INPUT_UNAVAILABLE_CUSTOMER_MESSAGE);
|
|
7448
7996
|
return;
|
|
@@ -7453,6 +8001,7 @@ var AgentRuntime = class {
|
|
|
7453
8001
|
skill
|
|
7454
8002
|
);
|
|
7455
8003
|
const runRecoveryExecution = async (history) => {
|
|
8004
|
+
let budgetExceeded = false;
|
|
7456
8005
|
let budgetTimer;
|
|
7457
8006
|
try {
|
|
7458
8007
|
const execPromise = skill.execute(
|
|
@@ -7474,6 +8023,7 @@ var AgentRuntime = class {
|
|
|
7474
8023
|
execPromise,
|
|
7475
8024
|
new Promise((_resolve, reject) => {
|
|
7476
8025
|
budgetTimer = setTimeout(() => {
|
|
8026
|
+
budgetExceeded = true;
|
|
7477
8027
|
recoveryAbort.abort();
|
|
7478
8028
|
reject(new ExecutionBudgetExceededError(recoveryBudgetMs));
|
|
7479
8029
|
}, recoveryBudgetMs);
|
|
@@ -7482,6 +8032,12 @@ var AgentRuntime = class {
|
|
|
7482
8032
|
}
|
|
7483
8033
|
return await execPromise;
|
|
7484
8034
|
} catch (err) {
|
|
8035
|
+
if (isHostScratchError(err)) {
|
|
8036
|
+
this.scratchFailed = true;
|
|
8037
|
+
}
|
|
8038
|
+
if (budgetExceeded) {
|
|
8039
|
+
throw new ExecutionBudgetExceededError(recoveryBudgetMs);
|
|
8040
|
+
}
|
|
7485
8041
|
this.markHealthFromExecuteError(skill, err, log, entry.job_id);
|
|
7486
8042
|
throw err;
|
|
7487
8043
|
} finally {
|
|
@@ -7494,18 +8050,28 @@ var AgentRuntime = class {
|
|
|
7494
8050
|
}
|
|
7495
8051
|
}
|
|
7496
8052
|
};
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
8053
|
+
let output;
|
|
8054
|
+
try {
|
|
8055
|
+
output = recoveryJobSession === null ? await runRecoveryExecution() : await this.runSessionExchange({
|
|
8056
|
+
session: recoveryJobSession,
|
|
8057
|
+
jobId: entry.job_id,
|
|
8058
|
+
userRecord: buildUserRecord(
|
|
8059
|
+
recoveryInputFile?.inlineText ?? entry.input,
|
|
8060
|
+
recoveryInputFile?.filePath !== void 0 ? "attachment" : void 0
|
|
8061
|
+
),
|
|
8062
|
+
signal: recoveryAbort.signal,
|
|
8063
|
+
log,
|
|
8064
|
+
execute: runRecoveryExecution,
|
|
8065
|
+
excludeOwnTurns: true
|
|
8066
|
+
});
|
|
8067
|
+
} catch (error) {
|
|
8068
|
+
if (!isScriptRefusalError(error)) {
|
|
8069
|
+
throw error;
|
|
8070
|
+
}
|
|
8071
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: ${describeForOperator(error)}`);
|
|
8072
|
+
await this.failRecoveredJob(entry, fakeJob, customerSafeMessage(error));
|
|
8073
|
+
return;
|
|
8074
|
+
}
|
|
7509
8075
|
const { attachments: resultAttachments, deliveredContent } = await this.buildResultAttachment(
|
|
7510
8076
|
entry.job_id,
|
|
7511
8077
|
output,
|
|
@@ -8028,7 +8594,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv, network) {
|
|
|
8028
8594
|
}
|
|
8029
8595
|
return skill;
|
|
8030
8596
|
}
|
|
8031
|
-
function
|
|
8597
|
+
function loadSkillsFromDir2(skillsDir, options) {
|
|
8032
8598
|
const skills = [];
|
|
8033
8599
|
let entries;
|
|
8034
8600
|
try {
|
|
@@ -8047,6 +8613,9 @@ function loadSkillsFromDir(skillsDir, options) {
|
|
|
8047
8613
|
}
|
|
8048
8614
|
const skillMdPath = join(entryPath, "SKILL.md");
|
|
8049
8615
|
try {
|
|
8616
|
+
if (isBlockingNodeSync(skillMdPath)) {
|
|
8617
|
+
throw new Error("it is a pipe, socket or device, not a file");
|
|
8618
|
+
}
|
|
8050
8619
|
const content = readFileSync(skillMdPath, "utf-8");
|
|
8051
8620
|
const { frontmatter, systemPrompt } = parseSkillMd(content);
|
|
8052
8621
|
const parsed = validateSkillFrontmatter(frontmatter, systemPrompt, {
|
|
@@ -8394,6 +8963,9 @@ function startWatchdog(deps) {
|
|
|
8394
8963
|
}
|
|
8395
8964
|
};
|
|
8396
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;
|
|
8397
8969
|
var X402_JOBS_FILE = ".x402-jobs.json";
|
|
8398
8970
|
var X402_RESULTS_DIR = ".x402-results";
|
|
8399
8971
|
function sanitizeJobId(jobId) {
|
|
@@ -8419,6 +8991,11 @@ var X402JobStore = class {
|
|
|
8419
8991
|
return scheduled;
|
|
8420
8992
|
}
|
|
8421
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
|
+
}
|
|
8422
8999
|
let raw;
|
|
8423
9000
|
try {
|
|
8424
9001
|
raw = await readFile(this.jobsPath, "utf-8");
|
|
@@ -8432,16 +9009,73 @@ var X402JobStore = class {
|
|
|
8432
9009
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
8433
9010
|
throw new Error(`x402 store ${this.jobsPath} is not a JSON object`);
|
|
8434
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
|
+
}
|
|
8435
9026
|
return parsed;
|
|
8436
9027
|
}
|
|
8437
9028
|
async save(file) {
|
|
8438
|
-
const tempPath = `${this.jobsPath}.tmp`;
|
|
8439
|
-
|
|
8440
|
-
|
|
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
|
+
}
|
|
8441
9044
|
}
|
|
8442
9045
|
resultFilePath(jobId) {
|
|
8443
9046
|
return join(this.resultsDir, sanitizeJobId(jobId));
|
|
8444
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
|
+
}
|
|
8445
9079
|
/**
|
|
8446
9080
|
* Atomically claim one paid attempt: increment iff still under BOTH caps
|
|
8447
9081
|
* (durable attempts and the monotonic signature count), and FLUSH before
|
|
@@ -8522,8 +9156,20 @@ var X402JobStore = class {
|
|
|
8522
9156
|
/** Binary result: bytes hit disk BEFORE the record flush (crash-safe ordering). */
|
|
8523
9157
|
async saveFileResult(jobId, mime, bytes) {
|
|
8524
9158
|
const filePath = this.resultFilePath(jobId);
|
|
8525
|
-
await mkdir(this.resultsDir, { recursive: true });
|
|
8526
|
-
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
|
+
}
|
|
8527
9173
|
await this.runExclusive(async () => {
|
|
8528
9174
|
const file = await this.load();
|
|
8529
9175
|
const now = Date.now();
|
|
@@ -8559,9 +9205,32 @@ var X402JobStore = class {
|
|
|
8559
9205
|
}
|
|
8560
9206
|
return { data: "", outputMime: result.mime, filePath };
|
|
8561
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
|
+
}
|
|
8562
9229
|
/** Drop records (and their result files) older than the cache TTL. */
|
|
8563
9230
|
async sweepExpired(now = Date.now()) {
|
|
8564
9231
|
await this.runExclusive(async () => {
|
|
9232
|
+
await this.sweepStrandedIndexTemporaries();
|
|
9233
|
+
await chmod(this.jobsPath, INDEX_FILE_MODE).catch(() => void 0);
|
|
8565
9234
|
const file = await this.load();
|
|
8566
9235
|
let changed = false;
|
|
8567
9236
|
for (const [jobId, record] of Object.entries(file)) {
|
|
@@ -8572,6 +9241,7 @@ var X402JobStore = class {
|
|
|
8572
9241
|
changed = true;
|
|
8573
9242
|
await rm(this.resultFilePath(jobId), { force: true }).catch(() => {
|
|
8574
9243
|
});
|
|
9244
|
+
await this.sweepStrandedTemporaries(jobId);
|
|
8575
9245
|
}
|
|
8576
9246
|
if (changed) {
|
|
8577
9247
|
await this.save(file);
|
|
@@ -8731,21 +9401,17 @@ function withPaymentIdentifier(headerValue, declaration, jobId) {
|
|
|
8731
9401
|
return headerValue;
|
|
8732
9402
|
}
|
|
8733
9403
|
}
|
|
8734
|
-
var UNICODE_FORMAT_MARKS = /\p{Cf}/gu;
|
|
8735
|
-
function flattenForOperator(text) {
|
|
8736
|
-
return sanitizeForTerminal(text).replace(UNICODE_FORMAT_MARKS, "").replace(/\s+/g, " ").trim();
|
|
8737
|
-
}
|
|
8738
9404
|
function printedPrefix(flattened) {
|
|
8739
|
-
return flattened
|
|
9405
|
+
return clipToCodeUnits(flattened, X402_ERROR_EXCERPT_CHARS);
|
|
8740
9406
|
}
|
|
8741
9407
|
function clipForOperator(flattened) {
|
|
8742
9408
|
return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : `${printedPrefix(flattened)}...`;
|
|
8743
9409
|
}
|
|
8744
9410
|
function quoteUpstream(text) {
|
|
8745
|
-
return clipForOperator(
|
|
9411
|
+
return clipForOperator(flattenForComparison(text));
|
|
8746
9412
|
}
|
|
8747
9413
|
function quoteUpstreamWithInput(text, sentInput) {
|
|
8748
|
-
return clipForOperator(maskCustomerInput(
|
|
9414
|
+
return clipForOperator(maskCustomerInput(flattenForComparison(text), inputForms(sentInput)));
|
|
8749
9415
|
}
|
|
8750
9416
|
function wireForm(sentInput) {
|
|
8751
9417
|
return new TextDecoder().decode(new TextEncoder().encode(sentInput));
|
|
@@ -8767,7 +9433,7 @@ function endsInsideInput(text, forms) {
|
|
|
8767
9433
|
}
|
|
8768
9434
|
function inputForms(sentInput) {
|
|
8769
9435
|
return [wireForm(sentInput), jsonEscapedForm(sentInput), ...urlEncodedForms(sentInput)].map(
|
|
8770
|
-
|
|
9436
|
+
flattenForComparison
|
|
8771
9437
|
);
|
|
8772
9438
|
}
|
|
8773
9439
|
function maskCustomerInput(text, forms) {
|
|
@@ -8811,7 +9477,7 @@ var X402Driver = class {
|
|
|
8811
9477
|
signerPromise = null;
|
|
8812
9478
|
repriceHintLogged = /* @__PURE__ */ new Set();
|
|
8813
9479
|
log(message) {
|
|
8814
|
-
(this.options.log ?? console.log)(`[x402] ${
|
|
9480
|
+
(this.options.log ?? console.log)(`[x402] ${flattenForComparison(message)}`);
|
|
8815
9481
|
}
|
|
8816
9482
|
getSigner() {
|
|
8817
9483
|
const secret = this.options.solanaSecretKeyBase58;
|
|
@@ -9015,7 +9681,7 @@ var X402Driver = class {
|
|
|
9015
9681
|
this.options.errorBodyReadMs ?? X402_ERROR_BODY_READ_MS
|
|
9016
9682
|
);
|
|
9017
9683
|
const forms = inputForms(sentInput);
|
|
9018
|
-
const quoted = maskCustomerInput(
|
|
9684
|
+
const quoted = maskCustomerInput(flattenForComparison(body.text), forms);
|
|
9019
9685
|
if (quoted.length === 0) {
|
|
9020
9686
|
return "";
|
|
9021
9687
|
}
|
|
@@ -9242,7 +9908,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9242
9908
|
const scriptEnv = buildScriptEnv(loaded.secrets);
|
|
9243
9909
|
const paths = agentPaths(loaded.dir);
|
|
9244
9910
|
const skillsDir = paths.skills;
|
|
9245
|
-
const allSkills =
|
|
9911
|
+
const allSkills = loadSkillsFromDir2(skillsDir, { network: walletNetwork, scriptEnv });
|
|
9246
9912
|
if (allSkills.length === 0) {
|
|
9247
9913
|
console.error(` ! No skills found in ${skillsDir}
|
|
9248
9914
|
`);
|
|
@@ -9265,6 +9931,31 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9265
9931
|
);
|
|
9266
9932
|
process.exit(1);
|
|
9267
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
|
+
}
|
|
9268
9959
|
const llmSkills = allSkills.filter((skill) => skill.mode === "llm");
|
|
9269
9960
|
const scriptDepSkills = allSkills.filter(
|
|
9270
9961
|
(skill) => skill.mode !== "llm" && skill.llmOverride?.provider && skill.llmOverride?.model
|
|
@@ -9449,6 +10140,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9449
10140
|
const x402Skills = allSkills.filter((skill) => skill.mode === "x402");
|
|
9450
10141
|
let x402InvariantBroken;
|
|
9451
10142
|
if (x402Skills.length > 0) {
|
|
10143
|
+
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
9452
10144
|
const solanaSecretKey = loaded.secrets.solana_secret_key;
|
|
9453
10145
|
if (solanaSecretKey === void 0 || solanaSecretKey.length === 0) {
|
|
9454
10146
|
x402InvariantBroken = "no solana_secret_key in .secrets.json";
|
|
@@ -9555,8 +10247,11 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9555
10247
|
}
|
|
9556
10248
|
}
|
|
9557
10249
|
if (mediaCacheDirty) {
|
|
10250
|
+
await ensureGitignoreHasPrivateStateEntries(dirname(loaded.dir));
|
|
9558
10251
|
await writeMediaCache(loaded.dir, mediaCache);
|
|
9559
10252
|
}
|
|
10253
|
+
await ensureGitignoreHasPrivateStateEntries(dirname(loaded.dir));
|
|
10254
|
+
const ledger = new JobLedger(paths.jobs);
|
|
9560
10255
|
let profilePublished = false;
|
|
9561
10256
|
try {
|
|
9562
10257
|
await client.discovery.publishProfile(
|
|
@@ -9667,7 +10362,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9667
10362
|
}
|
|
9668
10363
|
try {
|
|
9669
10364
|
await client.policies.deletePolicy(identity2, type);
|
|
9670
|
-
console.log(` Removed stale policy: ${
|
|
10365
|
+
console.log(` Removed stale policy: ${deleteControlCharacters(type)}`);
|
|
9671
10366
|
} catch {
|
|
9672
10367
|
}
|
|
9673
10368
|
}
|
|
@@ -9708,6 +10403,16 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9708
10403
|
console.warn(` ! Run \`npx @elisym/cli delegate-key ${loaded.name}\` to enable it.`);
|
|
9709
10404
|
}
|
|
9710
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
|
+
}
|
|
9711
10416
|
const buildCard = (skill) => buildCapabilityCard(skill, { walletNetwork, solanaAddress, delegatePubkey });
|
|
9712
10417
|
function retiredModelKeyForSkill(skill) {
|
|
9713
10418
|
if (skill.resolvedTriple) {
|
|
@@ -9778,7 +10483,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9778
10483
|
const card = JSON.parse(ev.content);
|
|
9779
10484
|
if (card.name && toDTag(card.name) === dTag) {
|
|
9780
10485
|
await client.discovery.deleteCapability(identity2, card.name);
|
|
9781
|
-
console.log(` Removed stale capability: ${
|
|
10486
|
+
console.log(` Removed stale capability: ${deleteControlCharacters(card.name)}`);
|
|
9782
10487
|
}
|
|
9783
10488
|
} catch {
|
|
9784
10489
|
}
|
|
@@ -9797,16 +10502,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9797
10502
|
});
|
|
9798
10503
|
};
|
|
9799
10504
|
const transport = new NostrTransport(client, identity2, [DEFAULT_KIND_OFFSET]);
|
|
9800
|
-
const ledger = new JobLedger(paths.jobs);
|
|
9801
10505
|
const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
|
|
9802
|
-
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
9803
|
-
if (x402Skills.length > 0) {
|
|
9804
|
-
await ensureGitignoreHasX402Entries(dirname(loaded.dir));
|
|
9805
|
-
}
|
|
9806
|
-
const hasContextSkills = registry.all().some((skill) => skill.context === true);
|
|
9807
|
-
if (hasContextSkills) {
|
|
9808
|
-
await ensureGitignoreHasSessionsEntry(dirname(loaded.dir));
|
|
9809
|
-
}
|
|
9810
10506
|
const runtimeConfig = {
|
|
9811
10507
|
paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
|
|
9812
10508
|
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
|
@@ -9821,10 +10517,6 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9821
10517
|
// the payment key) to execute post-work pulls from customer delegations.
|
|
9822
10518
|
delegateSigner
|
|
9823
10519
|
};
|
|
9824
|
-
const nonceStore = delegateSigner !== void 0 ? new UsedNonceStore(join(loaded.dir, ".delegation-nonces.json")) : void 0;
|
|
9825
|
-
if (nonceStore !== void 0) {
|
|
9826
|
-
await ensureGitignoreHasDelegationNoncesEntry(dirname(loaded.dir));
|
|
9827
|
-
}
|
|
9828
10520
|
const rpcUrlForLog = stripRpcSecrets(process.env.SOLANA_RPC_URL ?? getRpcUrl(walletNetwork));
|
|
9829
10521
|
logger.debug(
|
|
9830
10522
|
{
|
|
@@ -9871,7 +10563,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9871
10563
|
ledger,
|
|
9872
10564
|
{
|
|
9873
10565
|
onJobReceived: (job) => {
|
|
9874
|
-
const cap =
|
|
10566
|
+
const cap = deleteControlCharacters(job.tags.find((t) => t !== "elisym") ?? "unknown");
|
|
9875
10567
|
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
9876
10568
|
`);
|
|
9877
10569
|
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
@@ -9882,7 +10574,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9882
10574
|
logger.info({ event: "job_delivered", jobId });
|
|
9883
10575
|
},
|
|
9884
10576
|
onJobError: (jobId, error) => {
|
|
9885
|
-
const safeError =
|
|
10577
|
+
const safeError = deleteControlCharacters(error);
|
|
9886
10578
|
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${safeError}
|
|
9887
10579
|
`);
|
|
9888
10580
|
logger.error({ event: "job_error", jobId, error: safeError });
|
|
@@ -9974,37 +10666,6 @@ function buildScriptEnv(secrets) {
|
|
|
9974
10666
|
}
|
|
9975
10667
|
return scriptEnv;
|
|
9976
10668
|
}
|
|
9977
|
-
var PUBLIC_SOLANA_RPC_HOSTS = /* @__PURE__ */ new Set([
|
|
9978
|
-
"api.devnet.solana.com",
|
|
9979
|
-
"api.mainnet-beta.solana.com",
|
|
9980
|
-
"api.testnet.solana.com"
|
|
9981
|
-
]);
|
|
9982
|
-
function stripRpcSecrets(raw) {
|
|
9983
|
-
try {
|
|
9984
|
-
const parsed = new URL(raw);
|
|
9985
|
-
parsed.username = "";
|
|
9986
|
-
parsed.password = "";
|
|
9987
|
-
if (!PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname)) {
|
|
9988
|
-
return `${parsed.protocol}//${parsed.host}/***`;
|
|
9989
|
-
}
|
|
9990
|
-
const marker = parsed.search.length > 0 ? "?***" : "";
|
|
9991
|
-
parsed.search = "";
|
|
9992
|
-
return `${parsed.toString()}${marker}`;
|
|
9993
|
-
} catch {
|
|
9994
|
-
return "[unparseable RPC URL]";
|
|
9995
|
-
}
|
|
9996
|
-
}
|
|
9997
|
-
function isPublicSolanaRpcUrl(raw) {
|
|
9998
|
-
try {
|
|
9999
|
-
const parsed = new URL(raw);
|
|
10000
|
-
return PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname) && !parsed.username && !parsed.password && !parsed.search && (parsed.pathname === "" || parsed.pathname === "/");
|
|
10001
|
-
} catch {
|
|
10002
|
-
return false;
|
|
10003
|
-
}
|
|
10004
|
-
}
|
|
10005
|
-
function redactRpcUrlsInText(text) {
|
|
10006
|
-
return text.replace(/https?:\/\/[^\s)'"]+/g, (url) => stripRpcSecrets(url));
|
|
10007
|
-
}
|
|
10008
10669
|
async function resolveMediaField(value, agentDir, cache, blossom, identity2, onCacheUpdate) {
|
|
10009
10670
|
if (!value) {
|
|
10010
10671
|
return void 0;
|
|
@@ -10057,11 +10718,23 @@ async function uploadOrReuse(cacheKey, absPath, containRoot, cache, blossom, ide
|
|
|
10057
10718
|
);
|
|
10058
10719
|
return void 0;
|
|
10059
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
|
+
}
|
|
10060
10727
|
try {
|
|
10061
10728
|
const cached = await lookupCachedUrl(cache, cacheKey, realPath);
|
|
10062
10729
|
if (cached) {
|
|
10063
10730
|
return cached;
|
|
10064
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
|
+
}
|
|
10065
10738
|
console.log(` Uploading ${basename(realPath)}...`);
|
|
10066
10739
|
const data = readFileSync(realPath);
|
|
10067
10740
|
const sha256 = createHash("sha256").update(data).digest("hex");
|