@elisym/cli 0.20.0 → 0.21.1
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 +314 -53
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,19 +2,22 @@
|
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, resolve, basename, relative, sep } from 'node:path';
|
|
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, LIMITS, calculateProtocolFee, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
6
|
-
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';
|
|
7
7
|
import { isAddress, createSolanaRpc, address } from '@solana/kit';
|
|
8
8
|
import { generateSecretKey, getPublicKey, nip19, verifyEvent } from 'nostr-tools';
|
|
9
9
|
import YAML from 'yaml';
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import { createHash } from 'node:crypto';
|
|
12
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';
|
|
13
14
|
import { lookup } from 'node:dns/promises';
|
|
14
15
|
import { Socket } from 'node:net';
|
|
15
16
|
import pino from 'pino';
|
|
17
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
16
19
|
import pLimit from 'p-limit';
|
|
17
|
-
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';
|
|
18
21
|
import { fileURLToPath } from 'node:url';
|
|
19
22
|
|
|
20
23
|
var __defProp = Object.defineProperty;
|
|
@@ -1847,6 +1850,20 @@ var JobLedger = class {
|
|
|
1847
1850
|
this.flush();
|
|
1848
1851
|
}
|
|
1849
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
|
+
}
|
|
1850
1867
|
markDelivered(jobId) {
|
|
1851
1868
|
const entry = this.transition(jobId, "delivered");
|
|
1852
1869
|
if (entry) {
|
|
@@ -1981,6 +1998,9 @@ function resolveProviderApiKey(input) {
|
|
|
1981
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}.`
|
|
1982
1999
|
};
|
|
1983
2000
|
}
|
|
2001
|
+
function sanitizeForTerminal(value) {
|
|
2002
|
+
return value.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
2003
|
+
}
|
|
1984
2004
|
function resolveLevel(options) {
|
|
1985
2005
|
if (options.level) {
|
|
1986
2006
|
return options.level;
|
|
@@ -2019,7 +2039,7 @@ function createLogger(options = {}) {
|
|
|
2019
2039
|
logger = pino(baseOptions, pino.destination(2));
|
|
2020
2040
|
}
|
|
2021
2041
|
function logWithIndent(line) {
|
|
2022
|
-
process.stdout.write(` ${line}
|
|
2042
|
+
process.stdout.write(` ${sanitizeForTerminal(line)}
|
|
2023
2043
|
`);
|
|
2024
2044
|
}
|
|
2025
2045
|
return {
|
|
@@ -2117,7 +2137,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
|
2117
2137
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
2118
2138
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
2119
2139
|
var AgentRuntime = class {
|
|
2120
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor) {
|
|
2140
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport) {
|
|
2121
2141
|
this.transport = transport;
|
|
2122
2142
|
this.skills = skills;
|
|
2123
2143
|
this.skillCtx = skillCtx;
|
|
@@ -2125,6 +2145,7 @@ var AgentRuntime = class {
|
|
|
2125
2145
|
this.ledger = ledger;
|
|
2126
2146
|
this.callbacks = callbacks;
|
|
2127
2147
|
this.healthMonitor = healthMonitor;
|
|
2148
|
+
this.irohTransport = irohTransport;
|
|
2128
2149
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
2129
2150
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
2130
2151
|
}
|
|
@@ -2361,7 +2382,7 @@ var AgentRuntime = class {
|
|
|
2361
2382
|
});
|
|
2362
2383
|
return;
|
|
2363
2384
|
}
|
|
2364
|
-
const isFreeLlm = matched?.mode === "llm"
|
|
2385
|
+
const isFreeLlm = matched?.priceSubunits === 0 && (matched.mode === "llm" || resolveHealthPair(matched) !== null);
|
|
2365
2386
|
let perCustomerLimiter;
|
|
2366
2387
|
let perSkillKey;
|
|
2367
2388
|
if (isFreeLlm && matched) {
|
|
@@ -2408,6 +2429,11 @@ var AgentRuntime = class {
|
|
|
2408
2429
|
shuttingDown = true;
|
|
2409
2430
|
log("Shutting down...");
|
|
2410
2431
|
this.stop();
|
|
2432
|
+
const shutdownNode = this.irohTransport?.shutdown() ?? Promise.resolve();
|
|
2433
|
+
void shutdownNode.then(
|
|
2434
|
+
() => process.exit(0),
|
|
2435
|
+
() => process.exit(0)
|
|
2436
|
+
);
|
|
2411
2437
|
setTimeout(() => process.exit(0), 3e3).unref();
|
|
2412
2438
|
};
|
|
2413
2439
|
process.on("SIGINT", onSignal);
|
|
@@ -2493,8 +2519,9 @@ var AgentRuntime = class {
|
|
|
2493
2519
|
/** Core job processing logic - payment, skill execution, result delivery. */
|
|
2494
2520
|
async executeJob(job, signal) {
|
|
2495
2521
|
const log = this.callbacks.onLog ?? console.log;
|
|
2496
|
-
|
|
2497
|
-
|
|
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})`);
|
|
2498
2525
|
}
|
|
2499
2526
|
const matched = this.skills.route(job.tags);
|
|
2500
2527
|
const healthPair = resolveHealthPair(matched);
|
|
@@ -2512,6 +2539,12 @@ var AgentRuntime = class {
|
|
|
2512
2539
|
return;
|
|
2513
2540
|
}
|
|
2514
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
|
+
}
|
|
2515
2548
|
const jobPrice = resolveJobPrice(job.tags, this.skills);
|
|
2516
2549
|
const jobAsset = resolveJobAsset(job.tags, this.skills);
|
|
2517
2550
|
let netAmount;
|
|
@@ -2540,6 +2573,7 @@ var AgentRuntime = class {
|
|
|
2540
2573
|
);
|
|
2541
2574
|
this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
|
|
2542
2575
|
}
|
|
2576
|
+
const inputFile = await this.resolveInputFile(job.attachment);
|
|
2543
2577
|
await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
|
|
2544
2578
|
});
|
|
2545
2579
|
const skill = this.skills.route(job.tags);
|
|
@@ -2563,10 +2597,11 @@ var AgentRuntime = class {
|
|
|
2563
2597
|
try {
|
|
2564
2598
|
const execPromise = skill.execute(
|
|
2565
2599
|
{
|
|
2566
|
-
data: job.input,
|
|
2600
|
+
data: inputFile?.inlineText ?? job.input,
|
|
2567
2601
|
inputType: job.inputType,
|
|
2568
2602
|
tags: job.tags,
|
|
2569
|
-
jobId: job.jobId
|
|
2603
|
+
jobId: job.jobId,
|
|
2604
|
+
filePath: inputFile?.filePath
|
|
2570
2605
|
},
|
|
2571
2606
|
{ ...this.skillCtx, signal: execAbort.signal }
|
|
2572
2607
|
);
|
|
@@ -2603,14 +2638,145 @@ var AgentRuntime = class {
|
|
|
2603
2638
|
if (signal) {
|
|
2604
2639
|
signal.removeEventListener("abort", onOuterAbort);
|
|
2605
2640
|
}
|
|
2641
|
+
if (inputFile) {
|
|
2642
|
+
await inputFile.cleanup().catch(() => {
|
|
2643
|
+
});
|
|
2644
|
+
}
|
|
2606
2645
|
}
|
|
2607
|
-
this.
|
|
2646
|
+
const { attachment, deliveredContent } = await this.buildResultAttachment(job.jobId, output);
|
|
2647
|
+
this.ledger.markExecuted(job.jobId, deliveredContent);
|
|
2608
2648
|
log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
|
|
2609
|
-
const eventId = await this.transport.deliverResult(
|
|
2649
|
+
const eventId = await this.transport.deliverResult(
|
|
2650
|
+
job,
|
|
2651
|
+
deliveredContent,
|
|
2652
|
+
netAmount,
|
|
2653
|
+
attachment
|
|
2654
|
+
);
|
|
2610
2655
|
this.ledger.markDelivered(job.jobId);
|
|
2611
2656
|
log(`[${job.jobId.slice(0, 8)}] Delivered: ${eventId.slice(0, 16)}...`);
|
|
2612
2657
|
this.callbacks.onJobCompleted?.(job.jobId, output.data);
|
|
2613
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
|
+
}
|
|
2614
2780
|
/**
|
|
2615
2781
|
* Collect payment for a job. Creates payment request, sends PaymentRequired feedback,
|
|
2616
2782
|
* polls for on-chain confirmation. Aborts if signal fires.
|
|
@@ -2652,6 +2818,8 @@ var AgentRuntime = class {
|
|
|
2652
2818
|
}
|
|
2653
2819
|
const deadlineMs = this.config.paymentTimeoutSecs * 1e3;
|
|
2654
2820
|
const sigPathTimeoutMs = Math.min(deadlineMs, SIG_PATH_TIMEOUT_MS);
|
|
2821
|
+
let sigOutcome;
|
|
2822
|
+
let refOutcome;
|
|
2655
2823
|
let result;
|
|
2656
2824
|
try {
|
|
2657
2825
|
result = await new Promise((resolve3, reject) => {
|
|
@@ -2684,6 +2852,7 @@ var AgentRuntime = class {
|
|
|
2684
2852
|
return;
|
|
2685
2853
|
}
|
|
2686
2854
|
if (!sig) {
|
|
2855
|
+
sigOutcome = { gotSignature: false };
|
|
2687
2856
|
lose({ verified: false }, "sig path: no payment-completed feedback");
|
|
2688
2857
|
return;
|
|
2689
2858
|
}
|
|
@@ -2696,33 +2865,32 @@ var AgentRuntime = class {
|
|
|
2696
2865
|
win(verified);
|
|
2697
2866
|
} else {
|
|
2698
2867
|
const reason = verified.error ?? "unknown";
|
|
2868
|
+
sigOutcome = { gotSignature: true, error: reason };
|
|
2699
2869
|
lose(verified, `sig path: not verified (${reason})`);
|
|
2700
2870
|
}
|
|
2701
2871
|
} catch (err) {
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
);
|
|
2872
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2873
|
+
sigOutcome = { gotSignature: true, error: message };
|
|
2874
|
+
lose({ verified: false }, `sig path error: ${message}`);
|
|
2706
2875
|
}
|
|
2707
|
-
}).catch(
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
);
|
|
2876
|
+
}).catch((err) => {
|
|
2877
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2878
|
+
sigOutcome = { gotSignature: false, error: message };
|
|
2879
|
+
lose({ verified: false }, `sig path error: ${message}`);
|
|
2880
|
+
});
|
|
2713
2881
|
payment.verifyPayment(rpc, request, protocolConfig).then((verified) => {
|
|
2714
2882
|
if (verified.verified) {
|
|
2715
2883
|
win(verified);
|
|
2716
2884
|
} else {
|
|
2717
2885
|
const reason = verified.error ?? "unknown";
|
|
2886
|
+
refOutcome = { error: reason };
|
|
2718
2887
|
lose(verified, `ref path: not verified (${reason})`);
|
|
2719
2888
|
}
|
|
2720
|
-
}).catch(
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
);
|
|
2889
|
+
}).catch((err) => {
|
|
2890
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2891
|
+
refOutcome = { error: message };
|
|
2892
|
+
lose({ verified: false }, `ref path error: ${message}`);
|
|
2893
|
+
});
|
|
2726
2894
|
const deadline = setTimeout(() => {
|
|
2727
2895
|
if (settled) {
|
|
2728
2896
|
return;
|
|
@@ -2763,9 +2931,18 @@ var AgentRuntime = class {
|
|
|
2763
2931
|
if (result.verified) {
|
|
2764
2932
|
return { netAmount, paymentRequest: requestJson };
|
|
2765
2933
|
}
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2934
|
+
const refDefinitiveNoPay = refOutcome?.error === "No matching transaction found for reference key";
|
|
2935
|
+
const sigGotSignature = sigOutcome?.gotSignature === true;
|
|
2936
|
+
const customerAbandoned = !sigGotSignature && refDefinitiveNoPay;
|
|
2937
|
+
if (customerAbandoned) {
|
|
2938
|
+
log(
|
|
2939
|
+
`[${job.jobId.slice(0, 8)}] Payment not received; on-chain scan found no matching transaction - job abandoned by customer.`
|
|
2940
|
+
);
|
|
2941
|
+
} else {
|
|
2942
|
+
log(
|
|
2943
|
+
`[${job.jobId.slice(0, 8)}] WARNING: Payment verification timed out. Customer may have paid on-chain. Check address ${this.config.solanaAddress} manually.`
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2769
2946
|
await this.transport.sendFeedback(job, { type: "error", message: "payment timeout" }).catch(() => {
|
|
2770
2947
|
});
|
|
2771
2948
|
throw new Error("Payment timeout");
|
|
@@ -2851,6 +3028,9 @@ var AgentRuntime = class {
|
|
|
2851
3028
|
const recoveryAbort = new AbortController();
|
|
2852
3029
|
this.jobAbortControllers.add(recoveryAbort);
|
|
2853
3030
|
try {
|
|
3031
|
+
if (entry.raw_event_json === void 0) {
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
2854
3034
|
const rawEvent = JSON.parse(entry.raw_event_json);
|
|
2855
3035
|
const fakeJob = {
|
|
2856
3036
|
jobId: entry.job_id,
|
|
@@ -2863,7 +3043,15 @@ var AgentRuntime = class {
|
|
|
2863
3043
|
};
|
|
2864
3044
|
if (entry.status === "executed" && entry.result !== void 0) {
|
|
2865
3045
|
this.ledger.incrementRetry(entry.job_id);
|
|
2866
|
-
|
|
3046
|
+
let attachment;
|
|
3047
|
+
try {
|
|
3048
|
+
attachment = await this.reShareResultAttachment(entry.result_attachment);
|
|
3049
|
+
} catch {
|
|
3050
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: result blob unavailable, marking failed`);
|
|
3051
|
+
this.ledger.markFailed(entry.job_id);
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
await this.transport.deliverResult(fakeJob, entry.result, entry.net_amount, attachment);
|
|
2867
3055
|
this.ledger.markDelivered(entry.job_id);
|
|
2868
3056
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-delivered`);
|
|
2869
3057
|
} else if (entry.status === "paid") {
|
|
@@ -2906,16 +3094,27 @@ var AgentRuntime = class {
|
|
|
2906
3094
|
return;
|
|
2907
3095
|
}
|
|
2908
3096
|
}
|
|
3097
|
+
let recoveryInputFile;
|
|
3098
|
+
try {
|
|
3099
|
+
recoveryInputFile = await this.resolveInputFile(
|
|
3100
|
+
decodeJobPayload(rawEvent.content).attachment
|
|
3101
|
+
);
|
|
3102
|
+
} catch {
|
|
3103
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
|
|
3104
|
+
this.ledger.markFailed(entry.job_id);
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
2909
3107
|
const recoveryBudgetMs = this.resolveExecutionBudgetMs(skill);
|
|
2910
3108
|
let budgetTimer;
|
|
2911
3109
|
let output;
|
|
2912
3110
|
try {
|
|
2913
3111
|
const execPromise = skill.execute(
|
|
2914
3112
|
{
|
|
2915
|
-
data: entry.input,
|
|
3113
|
+
data: recoveryInputFile?.inlineText ?? entry.input,
|
|
2916
3114
|
inputType: entry.input_type,
|
|
2917
3115
|
tags: entry.tags,
|
|
2918
|
-
jobId: entry.job_id
|
|
3116
|
+
jobId: entry.job_id,
|
|
3117
|
+
filePath: recoveryInputFile?.filePath
|
|
2919
3118
|
},
|
|
2920
3119
|
{ ...this.skillCtx, signal: recoveryAbort.signal }
|
|
2921
3120
|
);
|
|
@@ -2938,9 +3137,22 @@ var AgentRuntime = class {
|
|
|
2938
3137
|
if (budgetTimer) {
|
|
2939
3138
|
clearTimeout(budgetTimer);
|
|
2940
3139
|
}
|
|
3140
|
+
if (recoveryInputFile) {
|
|
3141
|
+
await recoveryInputFile.cleanup().catch(() => {
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
2941
3144
|
}
|
|
2942
|
-
this.
|
|
2943
|
-
|
|
3145
|
+
const { attachment: resultAttachment, deliveredContent } = await this.buildResultAttachment(
|
|
3146
|
+
entry.job_id,
|
|
3147
|
+
output
|
|
3148
|
+
);
|
|
3149
|
+
this.ledger.markExecuted(entry.job_id, deliveredContent);
|
|
3150
|
+
await this.transport.deliverResult(
|
|
3151
|
+
fakeJob,
|
|
3152
|
+
deliveredContent,
|
|
3153
|
+
entry.net_amount,
|
|
3154
|
+
resultAttachment
|
|
3155
|
+
);
|
|
2944
3156
|
this.ledger.markDelivered(entry.job_id);
|
|
2945
3157
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: re-executed and delivered`);
|
|
2946
3158
|
}
|
|
@@ -3130,6 +3342,11 @@ var DynamicScriptSkill = class {
|
|
|
3130
3342
|
imageFile;
|
|
3131
3343
|
dir;
|
|
3132
3344
|
llmOverride;
|
|
3345
|
+
// Discovery hints surfaced in the published capability card (buildCard).
|
|
3346
|
+
// `outputMime` is also forwarded to the inner SDK runner (it labels the file
|
|
3347
|
+
// result); `inputMime` is publish-time metadata only.
|
|
3348
|
+
inputMime;
|
|
3349
|
+
outputMime;
|
|
3133
3350
|
inner;
|
|
3134
3351
|
constructor(params) {
|
|
3135
3352
|
this.name = params.name;
|
|
@@ -3141,6 +3358,8 @@ var DynamicScriptSkill = class {
|
|
|
3141
3358
|
this.imageFile = params.imageFile;
|
|
3142
3359
|
this.dir = params.dir;
|
|
3143
3360
|
this.llmOverride = params.llmOverride;
|
|
3361
|
+
this.inputMime = params.inputMime;
|
|
3362
|
+
this.outputMime = params.outputMime;
|
|
3144
3363
|
this.inner = new DynamicScriptSkill$1({
|
|
3145
3364
|
name: params.name,
|
|
3146
3365
|
description: params.description,
|
|
@@ -3152,7 +3371,8 @@ var DynamicScriptSkill = class {
|
|
|
3152
3371
|
scriptTimeoutMs: params.scriptTimeoutMs,
|
|
3153
3372
|
scriptEnv: params.scriptEnv,
|
|
3154
3373
|
image: params.image,
|
|
3155
|
-
imageFile: params.imageFile
|
|
3374
|
+
imageFile: params.imageFile,
|
|
3375
|
+
outputMime: params.outputMime
|
|
3156
3376
|
});
|
|
3157
3377
|
}
|
|
3158
3378
|
async execute(input, ctx) {
|
|
@@ -3227,6 +3447,13 @@ var ScriptSkill = class {
|
|
|
3227
3447
|
|
|
3228
3448
|
// src/skill/loader.ts
|
|
3229
3449
|
function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
3450
|
+
let safeImageFile = parsed.imageFile;
|
|
3451
|
+
if (safeImageFile !== void 0 && resolveInsidePath(entryPath, safeImageFile) === null) {
|
|
3452
|
+
console.warn(
|
|
3453
|
+
`SKILL.md "${parsed.name}": ignoring "image_file" that resolves outside the skill directory: ${safeImageFile}`
|
|
3454
|
+
);
|
|
3455
|
+
safeImageFile = void 0;
|
|
3456
|
+
}
|
|
3230
3457
|
let skill;
|
|
3231
3458
|
switch (parsed.mode) {
|
|
3232
3459
|
case "llm":
|
|
@@ -3237,7 +3464,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3237
3464
|
Number(parsed.priceSubunits),
|
|
3238
3465
|
parsed.asset,
|
|
3239
3466
|
parsed.image,
|
|
3240
|
-
|
|
3467
|
+
safeImageFile,
|
|
3241
3468
|
entryPath,
|
|
3242
3469
|
parsed.systemPrompt,
|
|
3243
3470
|
parsed.tools,
|
|
@@ -3265,7 +3492,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3265
3492
|
asset: parsed.asset,
|
|
3266
3493
|
outputFilePath,
|
|
3267
3494
|
image: parsed.image,
|
|
3268
|
-
imageFile:
|
|
3495
|
+
imageFile: safeImageFile,
|
|
3269
3496
|
dir: entryPath,
|
|
3270
3497
|
llmOverride: parsed.llmOverride
|
|
3271
3498
|
});
|
|
@@ -3282,8 +3509,7 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3282
3509
|
if (!scriptPath) {
|
|
3283
3510
|
throw new Error(`SKILL.md "${parsed.name}": "script" must stay inside the skill directory`);
|
|
3284
3511
|
}
|
|
3285
|
-
const
|
|
3286
|
-
skill = new Ctor({
|
|
3512
|
+
const scriptParams = {
|
|
3287
3513
|
name: parsed.name,
|
|
3288
3514
|
description: parsed.description,
|
|
3289
3515
|
capabilities: parsed.capabilities,
|
|
@@ -3294,10 +3520,15 @@ function buildCliSkill(parsed, entryPath, scriptEnv) {
|
|
|
3294
3520
|
scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
|
|
3295
3521
|
scriptEnv,
|
|
3296
3522
|
image: parsed.image,
|
|
3297
|
-
imageFile:
|
|
3523
|
+
imageFile: safeImageFile,
|
|
3298
3524
|
dir: entryPath,
|
|
3299
3525
|
llmOverride: parsed.llmOverride
|
|
3300
|
-
}
|
|
3526
|
+
};
|
|
3527
|
+
skill = parsed.mode === "dynamic-script" ? new DynamicScriptSkill({
|
|
3528
|
+
...scriptParams,
|
|
3529
|
+
outputMime: parsed.outputMime,
|
|
3530
|
+
inputMime: parsed.inputMime
|
|
3531
|
+
}) : new StaticScriptSkill(scriptParams);
|
|
3301
3532
|
break;
|
|
3302
3533
|
}
|
|
3303
3534
|
}
|
|
@@ -3344,6 +3575,13 @@ function loadSkillsFromDir(skillsDir, options = {}) {
|
|
|
3344
3575
|
function isEncrypted(event) {
|
|
3345
3576
|
return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
|
|
3346
3577
|
}
|
|
3578
|
+
function parseBidTag(value) {
|
|
3579
|
+
if (value === void 0) {
|
|
3580
|
+
return void 0;
|
|
3581
|
+
}
|
|
3582
|
+
const parsed = parseInt(value, 10);
|
|
3583
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3584
|
+
}
|
|
3347
3585
|
var HEALTH_CHECK_IDLE_MS = 30 * 60 * 1e3;
|
|
3348
3586
|
var NostrTransport = class {
|
|
3349
3587
|
constructor(client, identity, kindOffsets) {
|
|
@@ -3373,7 +3611,7 @@ var NostrTransport = class {
|
|
|
3373
3611
|
if (!hasElisym) {
|
|
3374
3612
|
return;
|
|
3375
3613
|
}
|
|
3376
|
-
const tags = event.tags.filter((
|
|
3614
|
+
const tags = event.tags.filter((tag) => tag[0] === "t").map((tag) => tag[1]).filter((value) => typeof value === "string");
|
|
3377
3615
|
const bidTag = event.tags.find((t) => t[0] === "bid");
|
|
3378
3616
|
const encrypted = isEncrypted(event);
|
|
3379
3617
|
const iTag = event.tags.find((t) => t[0] === "i");
|
|
@@ -3381,16 +3619,26 @@ var NostrTransport = class {
|
|
|
3381
3619
|
if (iTag?.[2]) {
|
|
3382
3620
|
inputType = iTag[2];
|
|
3383
3621
|
}
|
|
3384
|
-
const
|
|
3622
|
+
const rawInput = encrypted ? event.content : iTag?.[1] ?? event.content;
|
|
3623
|
+
let input;
|
|
3624
|
+
let attachment;
|
|
3625
|
+
try {
|
|
3626
|
+
const decoded = decodeJobPayload(rawInput);
|
|
3627
|
+
input = decoded.text ?? "";
|
|
3628
|
+
attachment = decoded.attachment;
|
|
3629
|
+
} catch {
|
|
3630
|
+
return;
|
|
3631
|
+
}
|
|
3385
3632
|
onJob({
|
|
3386
3633
|
jobId: event.id,
|
|
3387
3634
|
input,
|
|
3388
3635
|
inputType,
|
|
3389
3636
|
tags,
|
|
3390
3637
|
customerId: event.pubkey,
|
|
3391
|
-
bid: bidTag?.[1]
|
|
3638
|
+
bid: parseBidTag(bidTag?.[1]),
|
|
3392
3639
|
encrypted,
|
|
3393
|
-
rawEvent: event
|
|
3640
|
+
rawEvent: event,
|
|
3641
|
+
attachment
|
|
3394
3642
|
});
|
|
3395
3643
|
}
|
|
3396
3644
|
);
|
|
@@ -3441,6 +3689,9 @@ var NostrTransport = class {
|
|
|
3441
3689
|
if (!verifyEvent(event)) {
|
|
3442
3690
|
return;
|
|
3443
3691
|
}
|
|
3692
|
+
if (event.pubkey !== customerPubkey) {
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3444
3695
|
const status = event.tags.find((t) => t[0] === "status")?.[1];
|
|
3445
3696
|
if (status !== "payment-completed") {
|
|
3446
3697
|
return;
|
|
@@ -3485,13 +3736,15 @@ var NostrTransport = class {
|
|
|
3485
3736
|
}
|
|
3486
3737
|
}
|
|
3487
3738
|
/** Deliver result to customer. Retries with exponential backoff via SDK. */
|
|
3488
|
-
async deliverResult(job, content, amount, retries = 3) {
|
|
3739
|
+
async deliverResult(job, content, amount, attachment, retries = 3) {
|
|
3489
3740
|
return this.client.marketplace.submitJobResultWithRetry(
|
|
3490
3741
|
this.identity,
|
|
3491
3742
|
job.rawEvent,
|
|
3492
3743
|
content,
|
|
3493
3744
|
amount,
|
|
3494
|
-
retries
|
|
3745
|
+
retries,
|
|
3746
|
+
void 0,
|
|
3747
|
+
attachment
|
|
3495
3748
|
);
|
|
3496
3749
|
}
|
|
3497
3750
|
/** Returns true if an event was received within the given idle window. */
|
|
@@ -4013,6 +4266,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4013
4266
|
capabilities: skill.capabilities,
|
|
4014
4267
|
image: skill.image,
|
|
4015
4268
|
...isStatic ? { static: true } : {},
|
|
4269
|
+
// File-exchange hints (dynamic-script only). `inputMime` lets clients that
|
|
4270
|
+
// cannot send files (the web app) gate the Buy button before payment.
|
|
4271
|
+
...skill.inputMime ? { inputMime: skill.inputMime } : {},
|
|
4272
|
+
...skill.outputMime ? { outputMime: skill.outputMime } : {},
|
|
4016
4273
|
payment: solanaAddress ? {
|
|
4017
4274
|
chain: "solana",
|
|
4018
4275
|
network: walletNetwork,
|
|
@@ -4078,6 +4335,8 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4078
4335
|
};
|
|
4079
4336
|
const transport = new NostrTransport(client, identity, [DEFAULT_KIND_OFFSET]);
|
|
4080
4337
|
const ledger = new JobLedger(paths.jobs);
|
|
4338
|
+
const irohTransport = createIrohTransport({ storePath: join(loaded.dir, ".iroh") });
|
|
4339
|
+
await ensureGitignoreHasIrohEntry(dirname(loaded.dir));
|
|
4081
4340
|
const runtimeConfig = {
|
|
4082
4341
|
paymentTimeoutSecs: DEFAULTS.PAYMENT_EXPIRY_SECS,
|
|
4083
4342
|
maxConcurrentJobs: MAX_CONCURRENT_JOBS,
|
|
@@ -4130,7 +4389,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4130
4389
|
ledger,
|
|
4131
4390
|
{
|
|
4132
4391
|
onJobReceived: (job) => {
|
|
4133
|
-
const cap = job.tags.find((t) => t !== "elisym") ?? "unknown";
|
|
4392
|
+
const cap = sanitizeForTerminal(job.tags.find((t) => t !== "elisym") ?? "unknown");
|
|
4134
4393
|
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
4135
4394
|
`);
|
|
4136
4395
|
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
@@ -4141,9 +4400,10 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4141
4400
|
logger.info({ event: "job_delivered", jobId });
|
|
4142
4401
|
},
|
|
4143
4402
|
onJobError: (jobId, error) => {
|
|
4144
|
-
|
|
4403
|
+
const safeError = sanitizeForTerminal(error);
|
|
4404
|
+
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${safeError}
|
|
4145
4405
|
`);
|
|
4146
|
-
logger.error({ event: "job_error", jobId, error });
|
|
4406
|
+
logger.error({ event: "job_error", jobId, error: safeError });
|
|
4147
4407
|
},
|
|
4148
4408
|
onLog: diagLog,
|
|
4149
4409
|
onStop: () => {
|
|
@@ -4151,7 +4411,8 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4151
4411
|
llmHeartbeat?.stop();
|
|
4152
4412
|
}
|
|
4153
4413
|
},
|
|
4154
|
-
healthMonitor
|
|
4414
|
+
healthMonitor,
|
|
4415
|
+
irohTransport
|
|
4155
4416
|
);
|
|
4156
4417
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
4157
4418
|
await runtime.run();
|