@elisym/cli 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env -S node --no-deprecation
|
|
2
|
-
import {
|
|
2
|
+
import { ReadableStream } from 'node:stream/web';
|
|
3
|
+
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { dirname, join, resolve, basename, relative, sep } from 'node:path';
|
|
4
|
-
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, POLICY_T_TAG, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, LIMITS, calculateProtocolFee, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
5
|
-
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, MediaService, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, POLICY_T_TAG, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, utf8ByteLength, LIMITS, calculateProtocolFee, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
6
|
+
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, lookupCachedUrl, newCacheEntry, writeMediaCache } from '@elisym/sdk/agent-store';
|
|
6
7
|
import { isAddress, createSolanaRpc, address } from '@solana/kit';
|
|
7
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
8
9
|
import YAML from 'yaml';
|
|
9
10
|
import { Command } from 'commander';
|
|
10
11
|
import { createHash } from 'node:crypto';
|
|
11
|
-
import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
|
|
12
|
+
import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet, ScriptBillingExhaustedError, ScriptExecutionError, FREE_LLM_GLOBAL_KEY, freeLlmCustomerKey, LlmHealthError } from '@elisym/sdk/llm-health';
|
|
13
|
+
import { createIrohTransport } from '@elisym/sdk/node';
|
|
12
14
|
import { lookup } from 'node:dns/promises';
|
|
13
15
|
import { Socket } from 'node:net';
|
|
14
16
|
import pino from 'pino';
|
|
17
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
15
19
|
import pLimit from 'p-limit';
|
|
16
|
-
import { parseSkillMd, validateSkillFrontmatter, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS,
|
|
20
|
+
import { parseSkillMd, validateSkillFrontmatter, resolveInsidePath, DEFAULT_SCRIPT_TIMEOUT_MS, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
17
21
|
import { fileURLToPath } from 'node:url';
|
|
18
22
|
|
|
19
23
|
var __defProp = Object.defineProperty;
|
|
@@ -25,8 +29,6 @@ var __export = (target, all) => {
|
|
|
25
29
|
for (var name in all)
|
|
26
30
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
27
31
|
};
|
|
28
|
-
|
|
29
|
-
// src/llm/providers/http.ts
|
|
30
32
|
function resolveLlmTimeoutMs() {
|
|
31
33
|
const raw = process.env.ELISYM_LLM_TIMEOUT_MS;
|
|
32
34
|
if (raw === void 0) {
|
|
@@ -71,12 +73,53 @@ async function fetchWithTimeout(url, init, signal) {
|
|
|
71
73
|
const timer = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
|
|
72
74
|
const onAbort = () => controller.abort();
|
|
73
75
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
let toreDown = false;
|
|
77
|
+
const teardown = () => {
|
|
78
|
+
if (toreDown) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
toreDown = true;
|
|
77
82
|
clearTimeout(timer);
|
|
78
83
|
signal?.removeEventListener("abort", onAbort);
|
|
84
|
+
};
|
|
85
|
+
let response;
|
|
86
|
+
try {
|
|
87
|
+
response = await fetch(url, { ...init, signal: controller.signal });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
teardown();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
const body = response.body;
|
|
93
|
+
if (!body || typeof body.getReader !== "function") {
|
|
94
|
+
teardown();
|
|
95
|
+
return response;
|
|
79
96
|
}
|
|
97
|
+
const reader = body.getReader();
|
|
98
|
+
const tappedStream = new ReadableStream({
|
|
99
|
+
async pull(streamController) {
|
|
100
|
+
try {
|
|
101
|
+
const { done, value } = await reader.read();
|
|
102
|
+
if (done) {
|
|
103
|
+
teardown();
|
|
104
|
+
streamController.close();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
streamController.enqueue(value);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
teardown();
|
|
110
|
+
streamController.error(error);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
cancel(reason) {
|
|
114
|
+
teardown();
|
|
115
|
+
return reader.cancel(reason);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return new Response(tappedStream, {
|
|
119
|
+
status: response.status,
|
|
120
|
+
statusText: response.statusText,
|
|
121
|
+
headers: response.headers
|
|
122
|
+
});
|
|
80
123
|
}
|
|
81
124
|
async function fetchWithRetry(url, init, signal) {
|
|
82
125
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -1721,6 +1764,8 @@ async function fetchUsdcBalance(rpc, owner) {
|
|
|
1721
1764
|
return 0n;
|
|
1722
1765
|
}
|
|
1723
1766
|
}
|
|
1767
|
+
var LEDGER_DIR_MODE = 448;
|
|
1768
|
+
var LEDGER_FILE_MODE = 384;
|
|
1724
1769
|
var VALID_TRANSITIONS = {
|
|
1725
1770
|
paid: ["executed", "failed"],
|
|
1726
1771
|
executed: ["delivered", "failed"],
|
|
@@ -1750,7 +1795,9 @@ var JobLedger = class {
|
|
|
1750
1795
|
if (e?.code !== "ENOENT") {
|
|
1751
1796
|
console.warn(` ! Ledger load warning: ${e?.message ?? "unknown error"}`);
|
|
1752
1797
|
try {
|
|
1753
|
-
|
|
1798
|
+
const backupPath = this.path + ".corrupt." + Date.now();
|
|
1799
|
+
renameSync(this.path, backupPath);
|
|
1800
|
+
chmodSync(backupPath, LEDGER_FILE_MODE);
|
|
1754
1801
|
} catch {
|
|
1755
1802
|
}
|
|
1756
1803
|
}
|
|
@@ -1758,11 +1805,12 @@ var JobLedger = class {
|
|
|
1758
1805
|
}
|
|
1759
1806
|
flush() {
|
|
1760
1807
|
const dir = dirname(this.path);
|
|
1761
|
-
mkdirSync(dir, { recursive: true });
|
|
1808
|
+
mkdirSync(dir, { recursive: true, mode: LEDGER_DIR_MODE });
|
|
1762
1809
|
const obj = Object.fromEntries(this.entries);
|
|
1763
1810
|
const tmp = this.path + ".tmp";
|
|
1764
|
-
writeFileSync(tmp, JSON.stringify(obj, null, 2));
|
|
1811
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2), { mode: LEDGER_FILE_MODE });
|
|
1765
1812
|
renameSync(tmp, this.path);
|
|
1813
|
+
chmodSync(this.path, LEDGER_FILE_MODE);
|
|
1766
1814
|
}
|
|
1767
1815
|
recordPaid(entry) {
|
|
1768
1816
|
if (this.entries.has(entry.job_id)) {
|
|
@@ -1802,6 +1850,20 @@ var JobLedger = class {
|
|
|
1802
1850
|
this.flush();
|
|
1803
1851
|
}
|
|
1804
1852
|
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Record the JSON-serialized file-result descriptor. Survives later
|
|
1855
|
+
* `markDelivered`/`markFailed` (which only null `result`).
|
|
1856
|
+
*/
|
|
1857
|
+
recordAttachment(jobId, fields) {
|
|
1858
|
+
const entry = this.entries.get(jobId);
|
|
1859
|
+
if (!entry) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
if (fields.resultAttachment !== void 0) {
|
|
1863
|
+
entry.result_attachment = fields.resultAttachment;
|
|
1864
|
+
}
|
|
1865
|
+
this.flush();
|
|
1866
|
+
}
|
|
1805
1867
|
markDelivered(jobId) {
|
|
1806
1868
|
const entry = this.transition(jobId, "delivered");
|
|
1807
1869
|
if (entry) {
|
|
@@ -1936,6 +1998,9 @@ function resolveProviderApiKey(input) {
|
|
|
1936
1998
|
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}.`
|
|
1937
1999
|
};
|
|
1938
2000
|
}
|
|
2001
|
+
function sanitizeForTerminal(value) {
|
|
2002
|
+
return value.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
2003
|
+
}
|
|
1939
2004
|
function resolveLevel(options) {
|
|
1940
2005
|
if (options.level) {
|
|
1941
2006
|
return options.level;
|
|
@@ -1974,7 +2039,7 @@ function createLogger(options = {}) {
|
|
|
1974
2039
|
logger = pino(baseOptions, pino.destination(2));
|
|
1975
2040
|
}
|
|
1976
2041
|
function logWithIndent(line) {
|
|
1977
|
-
process.stdout.write(` ${line}
|
|
2042
|
+
process.stdout.write(` ${sanitizeForTerminal(line)}
|
|
1978
2043
|
`);
|
|
1979
2044
|
}
|
|
1980
2045
|
return {
|
|
@@ -2026,6 +2091,22 @@ var ExecutionBudgetExceededError = class extends Error {
|
|
|
2026
2091
|
this.name = "ExecutionBudgetExceededError";
|
|
2027
2092
|
}
|
|
2028
2093
|
};
|
|
2094
|
+
var CUSTOMER_SAFE_MESSAGE_PREFIXES = ["Input too long", "No skill matched", "Payment timeout"];
|
|
2095
|
+
function customerSafeMessage(error) {
|
|
2096
|
+
if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError) {
|
|
2097
|
+
return error.message;
|
|
2098
|
+
}
|
|
2099
|
+
if (error instanceof ScriptExecutionError) {
|
|
2100
|
+
return error.message;
|
|
2101
|
+
}
|
|
2102
|
+
if (error instanceof ScriptBillingExhaustedError) {
|
|
2103
|
+
return AGENT_UNAVAILABLE_MESSAGE;
|
|
2104
|
+
}
|
|
2105
|
+
if (error instanceof Error && CUSTOMER_SAFE_MESSAGE_PREFIXES.some((prefix) => error.message.startsWith(prefix))) {
|
|
2106
|
+
return error.message;
|
|
2107
|
+
}
|
|
2108
|
+
return "Internal processing error";
|
|
2109
|
+
}
|
|
2029
2110
|
function bodyLooksLikeBilling3(body) {
|
|
2030
2111
|
const lower = body.toLowerCase();
|
|
2031
2112
|
return BILLING_BODY_MARKERS3.some((marker) => lower.includes(marker));
|
|
@@ -2056,7 +2137,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
|
2056
2137
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
2057
2138
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
2058
2139
|
var AgentRuntime = class {
|
|
2059
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor) {
|
|
2140
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport) {
|
|
2060
2141
|
this.transport = transport;
|
|
2061
2142
|
this.skills = skills;
|
|
2062
2143
|
this.skillCtx = skillCtx;
|
|
@@ -2064,6 +2145,7 @@ var AgentRuntime = class {
|
|
|
2064
2145
|
this.ledger = ledger;
|
|
2065
2146
|
this.callbacks = callbacks;
|
|
2066
2147
|
this.healthMonitor = healthMonitor;
|
|
2148
|
+
this.irohTransport = irohTransport;
|
|
2067
2149
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
2068
2150
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
2069
2151
|
}
|
|
@@ -2208,7 +2290,14 @@ var AgentRuntime = class {
|
|
|
2208
2290
|
return true;
|
|
2209
2291
|
}
|
|
2210
2292
|
if (skill.mode !== "llm") {
|
|
2211
|
-
|
|
2293
|
+
let message;
|
|
2294
|
+
if (err instanceof ScriptExecutionError) {
|
|
2295
|
+
message = err.detail;
|
|
2296
|
+
} else if (err instanceof Error) {
|
|
2297
|
+
message = err.message;
|
|
2298
|
+
} else {
|
|
2299
|
+
message = String(err);
|
|
2300
|
+
}
|
|
2212
2301
|
const provider = skill.llmOverride?.provider;
|
|
2213
2302
|
const model = skill.llmOverride?.model;
|
|
2214
2303
|
if (!provider || !model) {
|
|
@@ -2293,7 +2382,7 @@ var AgentRuntime = class {
|
|
|
2293
2382
|
});
|
|
2294
2383
|
return;
|
|
2295
2384
|
}
|
|
2296
|
-
const isFreeLlm = matched?.mode === "llm"
|
|
2385
|
+
const isFreeLlm = matched?.priceSubunits === 0 && (matched.mode === "llm" || resolveHealthPair(matched) !== null);
|
|
2297
2386
|
let perCustomerLimiter;
|
|
2298
2387
|
let perSkillKey;
|
|
2299
2388
|
if (isFreeLlm && matched) {
|
|
@@ -2340,6 +2429,11 @@ var AgentRuntime = class {
|
|
|
2340
2429
|
shuttingDown = true;
|
|
2341
2430
|
log("Shutting down...");
|
|
2342
2431
|
this.stop();
|
|
2432
|
+
const shutdownNode = this.irohTransport?.shutdown() ?? Promise.resolve();
|
|
2433
|
+
void shutdownNode.then(
|
|
2434
|
+
() => process.exit(0),
|
|
2435
|
+
() => process.exit(0)
|
|
2436
|
+
);
|
|
2343
2437
|
setTimeout(() => process.exit(0), 3e3).unref();
|
|
2344
2438
|
};
|
|
2345
2439
|
process.on("SIGINT", onSignal);
|
|
@@ -2413,15 +2507,9 @@ var AgentRuntime = class {
|
|
|
2413
2507
|
`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will re-execute when LLM pair recovers (24h cutoff).`
|
|
2414
2508
|
);
|
|
2415
2509
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
safeMessage = e.message;
|
|
2420
|
-
} else if (e.message?.includes("API")) {
|
|
2421
|
-
safeMessage = "Internal processing error";
|
|
2422
|
-
} else {
|
|
2423
|
-
safeMessage = e.message ?? "Unknown error";
|
|
2424
|
-
}
|
|
2510
|
+
const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
|
|
2511
|
+
this.callbacks.onJobError?.(job.jobId, operatorMessage);
|
|
2512
|
+
const safeMessage = customerSafeMessage(e);
|
|
2425
2513
|
await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
|
|
2426
2514
|
});
|
|
2427
2515
|
} finally {
|
|
@@ -2431,8 +2519,9 @@ var AgentRuntime = class {
|
|
|
2431
2519
|
/** Core job processing logic - payment, skill execution, result delivery. */
|
|
2432
2520
|
async executeJob(job, signal) {
|
|
2433
2521
|
const log = this.callbacks.onLog ?? console.log;
|
|
2434
|
-
|
|
2435
|
-
|
|
2522
|
+
const inputBytes = utf8ByteLength(job.input);
|
|
2523
|
+
if (inputBytes > LIMITS.MAX_INPUT_LENGTH) {
|
|
2524
|
+
throw new Error(`Input too long: ${inputBytes} bytes (max ${LIMITS.MAX_INPUT_LENGTH})`);
|
|
2436
2525
|
}
|
|
2437
2526
|
const matched = this.skills.route(job.tags);
|
|
2438
2527
|
const healthPair = resolveHealthPair(matched);
|
|
@@ -2450,6 +2539,12 @@ var AgentRuntime = class {
|
|
|
2450
2539
|
return;
|
|
2451
2540
|
}
|
|
2452
2541
|
}
|
|
2542
|
+
if (job.attachment !== void 0 && resolveJobPrice(job.tags, this.skills) === 0) {
|
|
2543
|
+
log(`[${job.jobId.slice(0, 8)}] Rejecting file input on a free skill`);
|
|
2544
|
+
await this.transport.sendFeedback(job, { type: "error", message: "File inputs require a paid skill." }).catch(() => {
|
|
2545
|
+
});
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2453
2548
|
const jobPrice = resolveJobPrice(job.tags, this.skills);
|
|
2454
2549
|
const jobAsset = resolveJobAsset(job.tags, this.skills);
|
|
2455
2550
|
let netAmount;
|
|
@@ -2478,6 +2573,7 @@ var AgentRuntime = class {
|
|
|
2478
2573
|
);
|
|
2479
2574
|
this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
|
|
2480
2575
|
}
|
|
2576
|
+
const inputFile = await this.resolveInputFile(job.attachment);
|
|
2481
2577
|
await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
|
|
2482
2578
|
});
|
|
2483
2579
|
const skill = this.skills.route(job.tags);
|
|
@@ -2501,10 +2597,11 @@ var AgentRuntime = class {
|
|
|
2501
2597
|
try {
|
|
2502
2598
|
const execPromise = skill.execute(
|
|
2503
2599
|
{
|
|
2504
|
-
data: job.input,
|
|
2600
|
+
data: inputFile?.inlineText ?? job.input,
|
|
2505
2601
|
inputType: job.inputType,
|
|
2506
2602
|
tags: job.tags,
|
|
2507
|
-
jobId: job.jobId
|
|
2603
|
+
jobId: job.jobId,
|
|
2604
|
+
filePath: inputFile?.filePath
|
|
2508
2605
|
},
|
|
2509
2606
|
{ ...this.skillCtx, signal: execAbort.signal }
|
|
2510
2607
|
);
|
|
@@ -2541,14 +2638,145 @@ var AgentRuntime = class {
|
|
|
2541
2638
|
if (signal) {
|
|
2542
2639
|
signal.removeEventListener("abort", onOuterAbort);
|
|
2543
2640
|
}
|
|
2641
|
+
if (inputFile) {
|
|
2642
|
+
await inputFile.cleanup().catch(() => {
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2544
2645
|
}
|
|
2545
|
-
this.
|
|
2646
|
+
const { attachment, deliveredContent } = await this.buildResultAttachment(job.jobId, output);
|
|
2647
|
+
this.ledger.markExecuted(job.jobId, deliveredContent);
|
|
2546
2648
|
log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
|
|
2547
|
-
const eventId = await this.transport.deliverResult(
|
|
2649
|
+
const eventId = await this.transport.deliverResult(
|
|
2650
|
+
job,
|
|
2651
|
+
deliveredContent,
|
|
2652
|
+
netAmount,
|
|
2653
|
+
attachment
|
|
2654
|
+
);
|
|
2548
2655
|
this.ledger.markDelivered(job.jobId);
|
|
2549
2656
|
log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
|
|
2550
2657
|
this.callbacks.onJobCompleted?.(job.jobId, output.data);
|
|
2551
2658
|
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Decide how a skill's result travels: inline text, a seeded file, or seeded
|
|
2661
|
+
* large text. Returns the attachment descriptor (if any) PLUS the content to
|
|
2662
|
+
* deliver on the wire - which is the EMPTY string whenever the payload was
|
|
2663
|
+
* spilled to iroh (file or large text), so the encrypted result event carries
|
|
2664
|
+
* only the ticket and never re-trips the NIP-44 byte cap. The single
|
|
2665
|
+
* `deliveredContent` value must drive `markExecuted`, `deliverResult`, and the
|
|
2666
|
+
* recovery re-delivery alike (so a crash-recovered spill re-delivers empty too).
|
|
2667
|
+
* Persists the descriptor so recovery can re-share + re-deliver.
|
|
2668
|
+
*
|
|
2669
|
+
* Must run BEFORE `markExecuted` so a seed failure leaves the job `paid` and
|
|
2670
|
+
* recovery re-executes (rather than re-delivering a dead ticket). Runs after the
|
|
2671
|
+
* `skill.execute` budget window, so a slow seed never trips `max_execution_secs`.
|
|
2672
|
+
*/
|
|
2673
|
+
async buildResultAttachment(jobId, output) {
|
|
2674
|
+
if (output.filePath !== void 0) {
|
|
2675
|
+
try {
|
|
2676
|
+
if (!this.irohTransport) {
|
|
2677
|
+
throw new Error("Skill produced a file result but iroh transport is unavailable.");
|
|
2678
|
+
}
|
|
2679
|
+
const seeded = await this.irohTransport.seedPath(output.filePath);
|
|
2680
|
+
const attachment = {
|
|
2681
|
+
name: basename(output.filePath),
|
|
2682
|
+
size: seeded.size,
|
|
2683
|
+
mime: output.outputMime ?? "application/octet-stream",
|
|
2684
|
+
transports: [{ kind: "iroh", ticket: seeded.ticket }]
|
|
2685
|
+
};
|
|
2686
|
+
this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
|
|
2687
|
+
return { attachment, deliveredContent: output.data };
|
|
2688
|
+
} finally {
|
|
2689
|
+
await output.cleanup?.().catch(() => {
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (utf8ByteLength(output.data) > LIMITS.MAX_ENCRYPTED_INLINE_BYTES) {
|
|
2694
|
+
if (!this.irohTransport) {
|
|
2695
|
+
throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
|
|
2696
|
+
}
|
|
2697
|
+
const seeded = await this.irohTransport.seedBytes(Buffer.from(output.data, "utf8"));
|
|
2698
|
+
const attachment = {
|
|
2699
|
+
name: "result.txt",
|
|
2700
|
+
size: seeded.size,
|
|
2701
|
+
mime: "text/plain",
|
|
2702
|
+
transports: [{ kind: "iroh", ticket: seeded.ticket }]
|
|
2703
|
+
};
|
|
2704
|
+
this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
|
|
2705
|
+
return { attachment, deliveredContent: "" };
|
|
2706
|
+
}
|
|
2707
|
+
return { attachment: void 0, deliveredContent: output.data };
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Rebuild a file-result attachment for crash-recovery re-delivery: re-share the
|
|
2711
|
+
* blob from the persistent store to mint a fresh ticket (the original ticket's
|
|
2712
|
+
* direct addresses go stale across a restart). Returns undefined for a text
|
|
2713
|
+
* result; throws if the blob is gone (caller marks the job failed).
|
|
2714
|
+
*/
|
|
2715
|
+
async reShareResultAttachment(resultAttachmentJson) {
|
|
2716
|
+
if (resultAttachmentJson === void 0) {
|
|
2717
|
+
return void 0;
|
|
2718
|
+
}
|
|
2719
|
+
if (!this.irohTransport) {
|
|
2720
|
+
throw new Error("Cannot recover a file result: iroh transport is unavailable.");
|
|
2721
|
+
}
|
|
2722
|
+
const stored = JSON.parse(resultAttachmentJson);
|
|
2723
|
+
const irohTransport = stored.transports.find((transport) => transport.kind === "iroh");
|
|
2724
|
+
if (!irohTransport) {
|
|
2725
|
+
throw new Error("Stored result attachment has no iroh transport.");
|
|
2726
|
+
}
|
|
2727
|
+
const freshTicket = await this.irohTransport.reShare(irohTransport.ticket);
|
|
2728
|
+
return { ...stored, transports: [{ kind: "iroh", ticket: freshTicket }] };
|
|
2729
|
+
}
|
|
2730
|
+
/**
|
|
2731
|
+
* Resolve a job's file/large-text input (when it carries an attachment) via iroh.
|
|
2732
|
+
* Only called post-payment - free + attachment is rejected in the `executeJob`
|
|
2733
|
+
* preflight. Two outcomes, both with a `cleanup` callback:
|
|
2734
|
+
* - `text/*` within `MAX_REINLINE_TEXT_BYTES`: fetched into memory and returned
|
|
2735
|
+
* as `inlineText`, transparently re-inlined into `SkillInput.data` so skills
|
|
2736
|
+
* are unchanged (cleanup is a no-op - nothing on disk).
|
|
2737
|
+
* - binary, or text over the ceiling: streamed to a fixed `input` name inside a
|
|
2738
|
+
* unique `mkdtemp` dir (the untrusted attachment `name` never touches the path),
|
|
2739
|
+
* returned as `filePath`.
|
|
2740
|
+
* `fetchToBytes`/`fetchToPath` enforce the real cap on the BLAKE3-verified size, so
|
|
2741
|
+
* an incorrect declared `size` cannot exceed the in-memory ceiling.
|
|
2742
|
+
*/
|
|
2743
|
+
async resolveInputFile(attachment) {
|
|
2744
|
+
if (attachment === void 0) {
|
|
2745
|
+
return void 0;
|
|
2746
|
+
}
|
|
2747
|
+
if (!this.irohTransport) {
|
|
2748
|
+
throw new Error("Job carries a file input but iroh transport is unavailable.");
|
|
2749
|
+
}
|
|
2750
|
+
const irohTransport = attachment.transports.find((transport) => transport.kind === "iroh");
|
|
2751
|
+
if (!irohTransport) {
|
|
2752
|
+
throw new Error("File input has no iroh transport.");
|
|
2753
|
+
}
|
|
2754
|
+
if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2755
|
+
const bytes = await this.irohTransport.fetchToBytes(irohTransport.ticket, {
|
|
2756
|
+
maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES
|
|
2757
|
+
});
|
|
2758
|
+
return {
|
|
2759
|
+
inlineText: Buffer.from(bytes).toString("utf8"),
|
|
2760
|
+
cleanup: async () => {
|
|
2761
|
+
}
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
|
|
2765
|
+
const filePath = join(dir, "input");
|
|
2766
|
+
try {
|
|
2767
|
+
await this.irohTransport.fetchToPath(irohTransport.ticket, filePath);
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
2770
|
+
});
|
|
2771
|
+
throw error;
|
|
2772
|
+
}
|
|
2773
|
+
return {
|
|
2774
|
+
filePath,
|
|
2775
|
+
cleanup: async () => {
|
|
2776
|
+
await rm(dir, { recursive: true, force: true });
|
|
2777
|
+
}
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2552
2780
|
/**
|
|
2553
2781
|
* Collect payment for a job. Creates payment request, sends PaymentRequired feedback,
|
|
2554
2782
|
* polls for on-chain confirmation. Aborts if signal fires.
|
|
@@ -2789,6 +3017,9 @@ var AgentRuntime = class {
|
|
|
2789
3017
|
const recoveryAbort = new AbortController();
|
|
2790
3018
|
this.jobAbortControllers.add(recoveryAbort);
|
|
2791
3019
|
try {
|
|
3020
|
+
if (entry.raw_event_json === void 0) {
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
2792
3023
|
const rawEvent = JSON.parse(entry.raw_event_json);
|
|
2793
3024
|
const fakeJob = {
|
|
2794
3025
|
jobId: entry.job_id,
|
|
@@ -2801,7 +3032,15 @@ var AgentRuntime = class {
|
|
|
2801
3032
|
};
|
|
2802
3033
|
if (entry.status === "executed" && entry.result !== void 0) {
|
|
2803
3034
|
this.ledger.incrementRetry(entry.job_id);
|
|
2804
|
-
|
|
3035
|
+
let attachment;
|
|
3036
|
+
try {
|
|
3037
|
+
attachment = await this.reShareResultAttachment(entry.result_attachment);
|
|
3038
|
+
} catch {
|
|
3039
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: result blob unavailable, marking failed`);
|
|
3040
|
+
this.ledger.markFailed(entry.job_id);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
await this.transport.deliverResult(fakeJob, entry.result, entry.net_amount, attachment);
|
|
2805
3044
|
this.ledger.markDelivered(entry.job_id);
|
|
2806
3045
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
|
|
2807
3046
|
} else if (entry.status === "paid") {
|
|
@@ -2844,16 +3083,27 @@ var AgentRuntime = class {
|
|
|
2844
3083
|
return;
|
|
2845
3084
|
}
|
|
2846
3085
|
}
|
|
3086
|
+
let recoveryInputFile;
|
|
3087
|
+
try {
|
|
3088
|
+
recoveryInputFile = await this.resolveInputFile(
|
|
3089
|
+
decodeJobPayload(rawEvent.content).attachment
|
|
3090
|
+
);
|
|
3091
|
+
} catch {
|
|
3092
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
|
|
3093
|
+
this.ledger.markFailed(entry.job_id);
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
2847
3096
|
const recoveryBudgetMs = this.resolveExecutionBudgetMs(skill);
|
|
2848
3097
|
let budgetTimer;
|
|
2849
3098
|
let output;
|
|
2850
3099
|
try {
|
|
2851
3100
|
const execPromise = skill.execute(
|
|
2852
3101
|
{
|
|
2853
|
-
data: entry.input,
|
|
3102
|
+
data: recoveryInputFile?.inlineText ?? entry.input,
|
|
2854
3103
|
inputType: entry.input_type,
|
|
2855
3104
|
tags: entry.tags,
|
|
2856
|
-
jobId: entry.job_id
|
|
3105
|
+
jobId: entry.job_id,
|
|
3106
|
+
filePath: recoveryInputFile?.filePath
|
|
2857
3107
|
},
|
|
2858
3108
|
{ ...this.skillCtx, signal: recoveryAbort.signal }
|
|
2859
3109
|
);
|
|
@@ -2876,9 +3126,22 @@ var AgentRuntime = class {
|
|
|
2876
3126
|
if (budgetTimer) {
|
|
2877
3127
|
clearTimeout(budgetTimer);
|
|
2878
3128
|
}
|
|
3129
|
+
if (recoveryInputFile) {
|
|
3130
|
+
await recoveryInputFile.cleanup().catch(() => {
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
2879
3133
|
}
|
|
2880
|
-
this.
|
|
2881
|
-
|
|
3134
|
+
const { attachment: resultAttachment, deliveredContent } = await this.buildResultAttachment(
|
|
3135
|
+
entry.job_id,
|
|
3136
|
+
output
|
|
3137
|
+
);
|
|
3138
|
+
this.ledger.markExecuted(entry.job_id, deliveredContent);
|
|
3139
|
+
await this.transport.deliverResult(
|
|
3140
|
+
fakeJob,
|
|
3141
|
+
deliveredContent,
|
|
3142
|
+
entry.net_amount,
|
|
3143
|
+
resultAttachment
|
|
3144
|
+
);
|
|
2882
3145
|
this.ledger.markDelivered(entry.job_id);
|
|
2883
3146
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
|
|
2884
3147
|
}
|
|
@@ -3068,6 +3331,11 @@ var DynamicScriptSkill = class {
|
|
|
3068
3331
|
imageFile;
|
|
3069
3332
|
dir;
|
|
3070
3333
|
llmOverride;
|
|
3334
|
+
// Discovery hints surfaced in the published capability card (buildCard).
|
|
3335
|
+
// `outputMime` is also forwarded to the inner SDK runner (it labels the file
|
|
3336
|
+
// result); `inputMime` is publish-time metadata only.
|
|
3337
|
+
inputMime;
|
|
3338
|
+
outputMime;
|
|
3071
3339
|
inner;
|
|
3072
3340
|
constructor(params) {
|
|
3073
3341
|
this.name = params.name;
|
|
@@ -3079,6 +3347,8 @@ var DynamicScriptSkill = class {
|
|
|
3079
3347
|
this.imageFile = params.imageFile;
|
|
3080
3348
|
this.dir = params.dir;
|
|
3081
3349
|
this.llmOverride = params.llmOverride;
|
|
3350
|
+
this.inputMime = params.inputMime;
|
|
3351
|
+
this.outputMime = params.outputMime;
|
|
3082
3352
|
this.inner = new DynamicScriptSkill$1({
|
|
3083
3353
|
name: params.name,
|
|
3084
3354
|
description: params.description,
|
|
@@ -3090,7 +3360,8 @@ var DynamicScriptSkill = class {
|
|
|
3090
3360
|
scriptTimeoutMs: params.scriptTimeoutMs,
|
|
3091
3361
|
scriptEnv: params.scriptEnv,
|
|
3092
3362
|
image: params.image,
|
|
3093
|
-
imageFile: params.imageFile
|
|
3363
|
+
imageFile: params.imageFile,
|
|
3364
|
+
outputMime: params.outputMime
|
|
3094
3365
|
});
|
|
3095
3366
|
}
|
|
3096
3367
|
async execute(input, ctx) {
|
|
@@ -3165,6 +3436,13 @@ var ScriptSkill = class {
|
|
|
3165
3436
|
|
|
3166
3437
|
// src/skill/loader.ts
|
|
3167
3438
|
function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
3439
|
+
let safeImageFile = parsed.imageFile;
|
|
3440
|
+
if (safeImageFile !== void 0 && resolveInsidePath(entryPath, safeImageFile) === null) {
|
|
3441
|
+
console.warn(
|
|
3442
|
+
`SKILL.md "${parsed.name}": ignoring "image_file" that resolves outside the skill directory: ${safeImageFile}`
|
|
3443
|
+
);
|
|
3444
|
+
safeImageFile = void 0;
|
|
3445
|
+
}
|
|
3168
3446
|
let skill;
|
|
3169
3447
|
switch (parsed.mode) {
|
|
3170
3448
|
case "llm":
|
|
@@ -3175,7 +3453,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3175
3453
|
Number(parsed.priceSubunits),
|
|
3176
3454
|
parsed.asset,
|
|
3177
3455
|
parsed.image,
|
|
3178
|
-
|
|
3456
|
+
safeImageFile,
|
|
3179
3457
|
entryPath,
|
|
3180
3458
|
parsed.systemPrompt,
|
|
3181
3459
|
parsed.tools,
|
|
@@ -3203,7 +3481,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3203
3481
|
asset: parsed.asset,
|
|
3204
3482
|
outputFilePath,
|
|
3205
3483
|
image: parsed.image,
|
|
3206
|
-
imageFile:
|
|
3484
|
+
imageFile: safeImageFile,
|
|
3207
3485
|
dir: entryPath,
|
|
3208
3486
|
llmOverride: parsed.llmOverride
|
|
3209
3487
|
});
|
|
@@ -3220,8 +3498,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3220
3498
|
if (!scriptPath) {
|
|
3221
3499
|
throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
|
|
3222
3500
|
}
|
|
3223
|
-
const
|
|
3224
|
-
skill = new Ctor({
|
|
3501
|
+
const scriptParams = {
|
|
3225
3502
|
name: parsed.name,
|
|
3226
3503
|
description: parsed.description,
|
|
3227
3504
|
capabilities: parsed.capabilities,
|
|
@@ -3232,10 +3509,15 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3232
3509
|
scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
|
|
3233
3510
|
scriptEnv,
|
|
3234
3511
|
image: parsed.image,
|
|
3235
|
-
imageFile:
|
|
3512
|
+
imageFile: safeImageFile,
|
|
3236
3513
|
dir: entryPath,
|
|
3237
3514
|
llmOverride: parsed.llmOverride
|
|
3238
|
-
}
|
|
3515
|
+
};
|
|
3516
|
+
skill = parsed.mode === "dynamic-script" ? new DynamicScriptSkill({
|
|
3517
|
+
...scriptParams,
|
|
3518
|
+
outputMime: parsed.outputMime,
|
|
3519
|
+
inputMime: parsed.inputMime
|
|
3520
|
+
}) : new StaticScriptSkill(scriptParams);
|
|
3239
3521
|
break;
|
|
3240
3522
|
}
|
|
3241
3523
|
}
|
|
@@ -3282,6 +3564,13 @@ function loadSkillsFromDir(skillsDir, options = {}) {
|
|
|
3282
3564
|
function isEncrypted(event) {
|
|
3283
3565
|
return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
|
|
3284
3566
|
}
|
|
3567
|
+
function parseBidTag(value) {
|
|
3568
|
+
if (value === void 0) {
|
|
3569
|
+
return void 0;
|
|
3570
|
+
}
|
|
3571
|
+
const parsed = parseInt(value, 10);
|
|
3572
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3573
|
+
}
|
|
3285
3574
|
var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
|
|
3286
3575
|
var NostrTransport = class {
|
|
3287
3576
|
constructor(client, identity, kindOffsets) {
|
|
@@ -3311,7 +3600,7 @@ var NostrTransport = class {
|
|
|
3311
3600
|
if (!hasElisym) {
|
|
3312
3601
|
return;
|
|
3313
3602
|
}
|
|
3314
|
-
const tags = event.tags.filter((
|
|
3603
|
+
const tags = event.tags.filter((tag) => tag[0] === "t").map((tag) => tag[1]).filter((value) => typeof value === "string");
|
|
3315
3604
|
const bidTag = event.tags.find((t) => t[0] === "bid");
|
|
3316
3605
|
const encrypted = isEncrypted(event);
|
|
3317
3606
|
const iTag = event.tags.find((t) => t[0] === "i");
|
|
@@ -3319,16 +3608,26 @@ var NostrTransport = class {
|
|
|
3319
3608
|
if (iTag?.[2]) {
|
|
3320
3609
|
inputType = iTag[2];
|
|
3321
3610
|
}
|
|
3322
|
-
const
|
|
3611
|
+
const rawInput = encrypted ? event.content : iTag?.[1] ?? event.content;
|
|
3612
|
+
let input;
|
|
3613
|
+
let attachment;
|
|
3614
|
+
try {
|
|
3615
|
+
const decoded = decodeJobPayload(rawInput);
|
|
3616
|
+
input = decoded.text ?? "";
|
|
3617
|
+
attachment = decoded.attachment;
|
|
3618
|
+
} catch {
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3323
3621
|
onJob({
|
|
3324
3622
|
jobId: event.id,
|
|
3325
3623
|
input,
|
|
3326
3624
|
inputType,
|
|
3327
3625
|
tags,
|
|
3328
3626
|
customerId: event.pubkey,
|
|
3329
|
-
bid: bidTag?.[1]
|
|
3627
|
+
bid: parseBidTag(bidTag?.[1]),
|
|
3330
3628
|
encrypted,
|
|
3331
|
-
rawEvent: event
|
|
3629
|
+
rawEvent: event,
|
|
3630
|
+
attachment
|
|
3332
3631
|
});
|
|
3333
3632
|
}
|
|
3334
3633
|
);
|
|
@@ -3379,6 +3678,9 @@ var NostrTransport = class {
|
|
|
3379
3678
|
if (!verifyEvent(event)) {
|
|
3380
3679
|
return;
|
|
3381
3680
|
}
|
|
3681
|
+
if (event.pubkey !== customerPubkey) {
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3382
3684
|
const status = event.tags.find((t) => t[0] === "status")?.[1];
|
|
3383
3685
|
if (status !== "payment-completed") {
|
|
3384
3686
|
return;
|
|
@@ -3423,13 +3725,15 @@ var NostrTransport = class {
|
|
|
3423
3725
|
}
|
|
3424
3726
|
}
|
|
3425
3727
|
/** Deliver result to customer. Retries with exponential backoff via SDK. */
|
|
3426
|
-
async deliverResult(job, content, amount, retries = 3) {
|
|
3728
|
+
async deliverResult(job, content, amount, attachment, retries = 3) {
|
|
3427
3729
|
return this.client.marketplace.submitJobResultWithRetry(
|
|
3428
3730
|
this.identity,
|
|
3429
3731
|
job.rawEvent,
|
|
3430
3732
|
content,
|
|
3431
3733
|
amount,
|
|
3432
|
-
retries
|
|
3734
|
+
retries,
|
|
3735
|
+
void 0,
|
|
3736
|
+
attachment
|
|
3433
3737
|
);
|
|
3434
3738
|
}
|
|
3435
3739
|
/** Returns true if an event was received within the given idle window. */
|
|
@@ -3608,7 +3912,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
3608
3912
|
console.log(` Network ${walletNetwork}`);
|
|
3609
3913
|
console.log(` Address ${solanaAddress}`);
|
|
3610
3914
|
if (process.env.SOLANA_RPC_URL) {
|
|
3611
|
-
console.log(` RPC ${process.env.SOLANA_RPC_URL} (custom)`);
|
|
3915
|
+
console.log(` RPC ${stripRpcSecrets(process.env.SOLANA_RPC_URL)} (custom)`);
|
|
3612
3916
|
}
|
|
3613
3917
|
console.log(` SOL ${formatSol(balance)} (${balance} lamports)`);
|
|
3614
3918
|
console.log(` USDC ${formatAssetAmount(USDC_SOLANA_DEVNET, usdcBalance)}`);
|
|
@@ -3951,6 +4255,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
3951
4255
|
capabilities: skill.capabilities,
|
|
3952
4256
|
image: skill.image,
|
|
3953
4257
|
...isStatic ? { static: true } : {},
|
|
4258
|
+
// File-exchange hints (dynamic-script only). `inputMime` lets clients that
|
|
4259
|
+
// cannot send files (the web app) gate the Buy button before payment.
|
|
4260
|
+
...skill.inputMime ? { inputMime: skill.inputMime } : {},
|
|
4261
|
+
...skill.outputMime ? { outputMime: skill.outputMime } : {},
|
|
3954
4262
|
payment: solanaAddress ? {
|
|
3955
4263
|
chain: "solana",
|
|
3956
4264
|
network: walletNetwork,
|
|
@@ -4016,6 +4324,8 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4016
4324
|
};
|
|
4017
4325
|
const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
|
|
4018
4326
|
const ledger = new JobLedger(paths.jobs);
|
|
4327
|
+
const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
|
|
4328
|
+
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
4019
4329
|
const runtimeConfig = {
|
|
4020
4330
|
paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
|
|
4021
4331
|
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
|
@@ -4068,7 +4378,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4068
4378
|
ledger,
|
|
4069
4379
|
{
|
|
4070
4380
|
onJobReceived: (job) => {
|
|
4071
|
-
const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
|
|
4381
|
+
const cap = sanitizeForTerminal(job.tags.find((t) => t !== "elisym") ?? "unknown");
|
|
4072
4382
|
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
4073
4383
|
`);
|
|
4074
4384
|
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
@@ -4079,9 +4389,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4079
4389
|
logger.info({ event: "job_delivered", jobId });
|
|
4080
4390
|
},
|
|
4081
4391
|
onJobError: (jobId, error) => {
|
|
4082
|
-
|
|
4392
|
+
const safeError = sanitizeForTerminal(error);
|
|
4393
|
+
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${safeError}
|
|
4083
4394
|
`);
|
|
4084
|
-
logger.error({ event: "job_error", jobId, error });
|
|
4395
|
+
logger.error({ event: "job_error", jobId, error: safeError });
|
|
4085
4396
|
},
|
|
4086
4397
|
onLog: diagLog,
|
|
4087
4398
|
onStop: () => {
|
|
@@ -4089,16 +4400,25 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4089
4400
|
llmHeartbeat?.stop();
|
|
4090
4401
|
}
|
|
4091
4402
|
},
|
|
4092
|
-
healthMonitor
|
|
4403
|
+
healthMonitor,
|
|
4404
|
+
irohTransport
|
|
4093
4405
|
);
|
|
4094
4406
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
4095
4407
|
await runtime.run();
|
|
4096
4408
|
}
|
|
4409
|
+
var PUBLIC_SOLANA_RPC_HOSTS = /* @__PURE__ */ new Set([
|
|
4410
|
+
"api.devnet.solana.com",
|
|
4411
|
+
"api.mainnet-beta.solana.com",
|
|
4412
|
+
"api.testnet.solana.com"
|
|
4413
|
+
]);
|
|
4097
4414
|
function stripRpcSecrets(raw) {
|
|
4098
4415
|
try {
|
|
4099
4416
|
const parsed = new URL(raw);
|
|
4100
4417
|
parsed.username = "";
|
|
4101
4418
|
parsed.password = "";
|
|
4419
|
+
if (!PUBLIC_SOLANA_RPC_HOSTS.has(parsed.hostname)) {
|
|
4420
|
+
return `${parsed.protocol}//${parsed.host}/***`;
|
|
4421
|
+
}
|
|
4102
4422
|
const marker = parsed.search.length > 0 ? "?***" : "";
|
|
4103
4423
|
parsed.search = "";
|
|
4104
4424
|
return `${parsed.toString()}${marker}`;
|