@elisym/cli 0.22.0 → 0.22.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 +136 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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, extname } from 'node:path';
|
|
5
|
-
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, utf8ByteLength, LIMITS, calculateProtocolFee, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
5
|
+
import { SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, formatSol, formatAssetAmount, USDC_SOLANA_DEVNET, ElisymClient, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, jobRequestKind, DEFAULT_KIND_OFFSET, toDTag, DEFAULTS, createBlossomTransport, makeCensor, DEFAULT_REDACT_PATHS, POLICY_T_TAG, createSlidingWindowLimiter, getProtocolProgramId, getProtocolConfig, utf8ByteLength, LIMITS, readAcceptedTransports, calculateProtocolFee, decodeJobPayload, BoundedSet, KIND_JOB_FEEDBACK, NATIVE_SOL } from '@elisym/sdk';
|
|
6
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';
|
|
@@ -14,7 +14,7 @@ import { createIrohTransport } from '@elisym/sdk/node';
|
|
|
14
14
|
import { lookup } from 'node:dns/promises';
|
|
15
15
|
import { Socket } from 'node:net';
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { mkdtemp, rm } from 'node:fs/promises';
|
|
17
|
+
import { readFile, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
18
18
|
import { tmpdir } from 'node:os';
|
|
19
19
|
import pLimit from 'p-limit';
|
|
20
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';
|
|
@@ -2152,7 +2152,7 @@ var PAID_GLOBAL_MAX_JOBS_PER_WINDOW = 2e3;
|
|
|
2152
2152
|
var MAX_TRACKED_CUSTOMERS = 1e3;
|
|
2153
2153
|
var GLOBAL_LIMITER_KEY = "__global__";
|
|
2154
2154
|
var AgentRuntime = class {
|
|
2155
|
-
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport) {
|
|
2155
|
+
constructor(transport, skills, skillCtx, config, ledger, callbacks = {}, healthMonitor, irohTransport, identity, blossomTransport) {
|
|
2156
2156
|
this.transport = transport;
|
|
2157
2157
|
this.skills = skills;
|
|
2158
2158
|
this.skillCtx = skillCtx;
|
|
@@ -2161,6 +2161,8 @@ var AgentRuntime = class {
|
|
|
2161
2161
|
this.callbacks = callbacks;
|
|
2162
2162
|
this.healthMonitor = healthMonitor;
|
|
2163
2163
|
this.irohTransport = irohTransport;
|
|
2164
|
+
this.identity = identity;
|
|
2165
|
+
this.blossomTransport = blossomTransport;
|
|
2164
2166
|
this.limit = pLimit(config.maxConcurrentJobs);
|
|
2165
2167
|
this.maxQueueSize = config.maxQueueSize ?? config.maxConcurrentJobs * 10;
|
|
2166
2168
|
}
|
|
@@ -2588,7 +2590,7 @@ var AgentRuntime = class {
|
|
|
2588
2590
|
);
|
|
2589
2591
|
this.callbacks.onPaymentReceived?.(job.jobId, netAmount);
|
|
2590
2592
|
}
|
|
2591
|
-
const inputFile = await this.resolveInputFile(job.attachment);
|
|
2593
|
+
const inputFile = await this.resolveInputFile(job.attachment, job.customerId);
|
|
2592
2594
|
await this.transport.sendFeedback(job, { type: "processing" }).catch(() => {
|
|
2593
2595
|
});
|
|
2594
2596
|
const skill = this.skills.route(job.tags);
|
|
@@ -2658,7 +2660,12 @@ var AgentRuntime = class {
|
|
|
2658
2660
|
});
|
|
2659
2661
|
}
|
|
2660
2662
|
}
|
|
2661
|
-
const { attachment, deliveredContent } = await this.buildResultAttachment(
|
|
2663
|
+
const { attachment, deliveredContent } = await this.buildResultAttachment(
|
|
2664
|
+
job.jobId,
|
|
2665
|
+
output,
|
|
2666
|
+
job.customerId,
|
|
2667
|
+
readAcceptedTransports(job.rawEvent.tags)
|
|
2668
|
+
);
|
|
2662
2669
|
this.ledger.markExecuted(job.jobId, deliveredContent);
|
|
2663
2670
|
log(`[${job.jobId.slice(0, 8)}] Skill completed, delivering result`);
|
|
2664
2671
|
const eventId = await this.transport.deliverResult(
|
|
@@ -2685,18 +2692,30 @@ var AgentRuntime = class {
|
|
|
2685
2692
|
* recovery re-executes (rather than re-delivering a dead ticket). Runs after the
|
|
2686
2693
|
* `skill.execute` budget window, so a slow seed never trips `max_execution_secs`.
|
|
2687
2694
|
*/
|
|
2688
|
-
async buildResultAttachment(jobId, output) {
|
|
2695
|
+
async buildResultAttachment(jobId, output, customerPubkey, acceptedTransports) {
|
|
2696
|
+
const wantBlossom = acceptedTransports === void 0 || acceptedTransports.includes("blossom");
|
|
2689
2697
|
if (output.filePath !== void 0) {
|
|
2690
2698
|
try {
|
|
2691
2699
|
if (!this.irohTransport) {
|
|
2692
2700
|
throw new Error("Skill produced a file result but iroh transport is unavailable.");
|
|
2693
2701
|
}
|
|
2694
2702
|
const seeded = await this.irohTransport.seedPath(output.filePath);
|
|
2703
|
+
const transports = [{ kind: "iroh", ticket: seeded.ticket }];
|
|
2704
|
+
if (wantBlossom) {
|
|
2705
|
+
const blossomMember = await this.seedBlossomFromPath(
|
|
2706
|
+
output.filePath,
|
|
2707
|
+
seeded.size,
|
|
2708
|
+
customerPubkey
|
|
2709
|
+
);
|
|
2710
|
+
if (blossomMember !== void 0) {
|
|
2711
|
+
transports.unshift(blossomMember);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2695
2714
|
const attachment = {
|
|
2696
2715
|
name: basename(output.filePath),
|
|
2697
2716
|
size: seeded.size,
|
|
2698
2717
|
mime: output.outputMime ?? "application/octet-stream",
|
|
2699
|
-
transports
|
|
2718
|
+
transports
|
|
2700
2719
|
};
|
|
2701
2720
|
this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
|
|
2702
2721
|
return { attachment, deliveredContent: output.data };
|
|
@@ -2709,18 +2728,60 @@ var AgentRuntime = class {
|
|
|
2709
2728
|
if (!this.irohTransport) {
|
|
2710
2729
|
throw new Error("Result is too large to deliver inline and iroh transport is unavailable.");
|
|
2711
2730
|
}
|
|
2712
|
-
const
|
|
2731
|
+
const dataBytes = Buffer.from(output.data, "utf8");
|
|
2732
|
+
const seeded = await this.irohTransport.seedBytes(dataBytes);
|
|
2733
|
+
const transports = [{ kind: "iroh", ticket: seeded.ticket }];
|
|
2734
|
+
if (wantBlossom) {
|
|
2735
|
+
const blossomMember = await this.seedBlossomMember(dataBytes, customerPubkey);
|
|
2736
|
+
if (blossomMember !== void 0) {
|
|
2737
|
+
transports.unshift(blossomMember);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2713
2740
|
const attachment = {
|
|
2714
2741
|
name: "result.txt",
|
|
2715
2742
|
size: seeded.size,
|
|
2716
2743
|
mime: "text/plain",
|
|
2717
|
-
transports
|
|
2744
|
+
transports
|
|
2718
2745
|
};
|
|
2719
2746
|
this.ledger.recordAttachment(jobId, { resultAttachment: JSON.stringify(attachment) });
|
|
2720
2747
|
return { attachment, deliveredContent: "" };
|
|
2721
2748
|
}
|
|
2722
2749
|
return { attachment: void 0, deliveredContent: output.data };
|
|
2723
2750
|
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Encrypt `bytes` to `recipientPubkey` and seed them to Blossom, returning a `blossom` transport
|
|
2753
|
+
* member - or `undefined` when blossom isn't configured, the bytes exceed the encrypted cap, or the
|
|
2754
|
+
* upload fails. Returning undefined (never throwing) keeps iroh as the guaranteed transport: an
|
|
2755
|
+
* optional second path must never fail the job.
|
|
2756
|
+
*/
|
|
2757
|
+
async seedBlossomMember(bytes, recipientPubkey) {
|
|
2758
|
+
if (this.blossomTransport === void 0 || this.identity === void 0) {
|
|
2759
|
+
return void 0;
|
|
2760
|
+
}
|
|
2761
|
+
if (bytes.byteLength > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
|
|
2762
|
+
return void 0;
|
|
2763
|
+
}
|
|
2764
|
+
try {
|
|
2765
|
+
return await this.blossomTransport.seedBytes({ bytes, recipientPubkey });
|
|
2766
|
+
} catch {
|
|
2767
|
+
return void 0;
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
/** Read a file (only when within the encrypted cap, to bound memory) and seed it to Blossom. */
|
|
2771
|
+
async seedBlossomFromPath(filePath, size, recipientPubkey) {
|
|
2772
|
+
if (this.blossomTransport === void 0 || this.identity === void 0) {
|
|
2773
|
+
return void 0;
|
|
2774
|
+
}
|
|
2775
|
+
if (size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
|
|
2776
|
+
return void 0;
|
|
2777
|
+
}
|
|
2778
|
+
try {
|
|
2779
|
+
const bytes = await readFile(filePath);
|
|
2780
|
+
return await this.seedBlossomMember(bytes, recipientPubkey);
|
|
2781
|
+
} catch {
|
|
2782
|
+
return void 0;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2724
2785
|
/**
|
|
2725
2786
|
* Rebuild a file-result attachment for crash-recovery re-delivery: re-share the
|
|
2726
2787
|
* blob from the persistent store to mint a fresh ticket (the original ticket's
|
|
@@ -2735,12 +2796,17 @@ var AgentRuntime = class {
|
|
|
2735
2796
|
throw new Error("Cannot recover a file result: iroh transport is unavailable.");
|
|
2736
2797
|
}
|
|
2737
2798
|
const stored = JSON.parse(resultAttachmentJson);
|
|
2738
|
-
const irohTransport = stored.transports.find(
|
|
2799
|
+
const irohTransport = stored.transports.find(
|
|
2800
|
+
(transport) => transport.kind === "iroh"
|
|
2801
|
+
);
|
|
2739
2802
|
if (!irohTransport) {
|
|
2740
2803
|
throw new Error("Stored result attachment has no iroh transport.");
|
|
2741
2804
|
}
|
|
2742
2805
|
const freshTicket = await this.irohTransport.reShare(irohTransport.ticket);
|
|
2743
|
-
|
|
2806
|
+
const transports = stored.transports.map(
|
|
2807
|
+
(transport) => transport.kind === "iroh" ? { kind: "iroh", ticket: freshTicket } : transport
|
|
2808
|
+
);
|
|
2809
|
+
return { ...stored, transports };
|
|
2744
2810
|
}
|
|
2745
2811
|
/**
|
|
2746
2812
|
* Resolve a job's file/large-text input (when it carries an attachment) via iroh.
|
|
@@ -2755,31 +2821,67 @@ var AgentRuntime = class {
|
|
|
2755
2821
|
* `fetchToBytes`/`fetchToPath` enforce the real cap on the BLAKE3-verified size, so
|
|
2756
2822
|
* an incorrect declared `size` cannot exceed the in-memory ceiling.
|
|
2757
2823
|
*/
|
|
2758
|
-
async resolveInputFile(attachment) {
|
|
2824
|
+
async resolveInputFile(attachment, senderPubkey) {
|
|
2759
2825
|
if (attachment === void 0) {
|
|
2760
2826
|
return void 0;
|
|
2761
2827
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2828
|
+
const irohMember = attachment.transports.find(
|
|
2829
|
+
(t) => t.kind === "iroh"
|
|
2830
|
+
);
|
|
2831
|
+
const blossomMember = attachment.transports.find(
|
|
2832
|
+
(t) => t.kind === "blossom"
|
|
2833
|
+
);
|
|
2834
|
+
if (irohMember !== void 0 && this.irohTransport !== void 0) {
|
|
2835
|
+
if (attachment.mime.startsWith("text/") && attachment.size <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2836
|
+
const bytes = await this.irohTransport.fetchToBytes(irohMember.ticket, {
|
|
2837
|
+
maxBytes: LIMITS.MAX_REINLINE_TEXT_BYTES
|
|
2838
|
+
});
|
|
2839
|
+
return this.materializeBytesInput(bytes, attachment.mime);
|
|
2840
|
+
}
|
|
2841
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
|
|
2842
|
+
const filePath = join(dir, "input");
|
|
2843
|
+
try {
|
|
2844
|
+
await this.irohTransport.fetchToPath(irohMember.ticket, filePath);
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
2847
|
+
});
|
|
2848
|
+
throw error;
|
|
2849
|
+
}
|
|
2773
2850
|
return {
|
|
2774
|
-
|
|
2851
|
+
filePath,
|
|
2775
2852
|
cleanup: async () => {
|
|
2853
|
+
await rm(dir, { recursive: true, force: true });
|
|
2776
2854
|
}
|
|
2777
2855
|
};
|
|
2778
2856
|
}
|
|
2857
|
+
if (blossomMember !== void 0 && this.blossomTransport !== void 0 && this.identity !== void 0) {
|
|
2858
|
+
if (attachment.size > LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES) {
|
|
2859
|
+
throw new Error("Blossom file input exceeds the encrypted size cap.");
|
|
2860
|
+
}
|
|
2861
|
+
const bytes = await this.blossomTransport.fetchToBytes({
|
|
2862
|
+
transport: blossomMember,
|
|
2863
|
+
senderPubkey,
|
|
2864
|
+
maxBytes: LIMITS.MAX_BLOSSOM_ENCRYPTED_BYTES
|
|
2865
|
+
});
|
|
2866
|
+
return this.materializeBytesInput(bytes, attachment.mime);
|
|
2867
|
+
}
|
|
2868
|
+
throw new Error("Job carries a file input but no supported transport is available.");
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* Turn fetched input bytes into a SkillInput: text within the re-inline ceiling becomes an
|
|
2872
|
+
* in-memory string; anything else is written to a fixed `input` name inside a unique mkdtemp dir
|
|
2873
|
+
* (the untrusted attachment `name` never touches the path). Shared by the iroh-text and blossom
|
|
2874
|
+
* paths (both have the bytes in hand); the iroh-binary path streams to disk separately.
|
|
2875
|
+
*/
|
|
2876
|
+
async materializeBytesInput(bytes, mime) {
|
|
2877
|
+
if (mime.startsWith("text/") && bytes.byteLength <= LIMITS.MAX_REINLINE_TEXT_BYTES) {
|
|
2878
|
+
return { inlineText: Buffer.from(bytes).toString("utf8"), cleanup: async () => {
|
|
2879
|
+
} };
|
|
2880
|
+
}
|
|
2779
2881
|
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"));
|
|
2780
2882
|
const filePath = join(dir, "input");
|
|
2781
2883
|
try {
|
|
2782
|
-
await
|
|
2884
|
+
await writeFile(filePath, bytes);
|
|
2783
2885
|
} catch (error) {
|
|
2784
2886
|
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
2785
2887
|
});
|
|
@@ -3112,7 +3214,8 @@ var AgentRuntime = class {
|
|
|
3112
3214
|
let recoveryInputFile;
|
|
3113
3215
|
try {
|
|
3114
3216
|
recoveryInputFile = await this.resolveInputFile(
|
|
3115
|
-
decodeJobPayload(rawEvent.content).attachment
|
|
3217
|
+
decodeJobPayload(rawEvent.content).attachment,
|
|
3218
|
+
entry.customer_id
|
|
3116
3219
|
);
|
|
3117
3220
|
} catch {
|
|
3118
3221
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
|
|
@@ -3159,7 +3262,9 @@ var AgentRuntime = class {
|
|
|
3159
3262
|
}
|
|
3160
3263
|
const { attachment: resultAttachment, deliveredContent } = await this.buildResultAttachment(
|
|
3161
3264
|
entry.job_id,
|
|
3162
|
-
output
|
|
3265
|
+
output,
|
|
3266
|
+
entry.customer_id,
|
|
3267
|
+
readAcceptedTransports(rawEvent.tags)
|
|
3163
3268
|
);
|
|
3164
3269
|
this.ledger.markExecuted(entry.job_id, deliveredContent);
|
|
3165
3270
|
await this.transport.deliverResult(
|
|
@@ -4445,6 +4550,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4445
4550
|
});
|
|
4446
4551
|
diagLog("LLM health monitor armed (lazy recovery, 5min interval).");
|
|
4447
4552
|
}
|
|
4553
|
+
const blossomTransport = createBlossomTransport({ blossom: client.blossom, identity });
|
|
4448
4554
|
const runtime = new AgentRuntime(
|
|
4449
4555
|
transport,
|
|
4450
4556
|
registry,
|
|
@@ -4476,7 +4582,9 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
4476
4582
|
}
|
|
4477
4583
|
},
|
|
4478
4584
|
healthMonitor,
|
|
4479
|
-
irohTransport
|
|
4585
|
+
irohTransport,
|
|
4586
|
+
identity,
|
|
4587
|
+
blossomTransport
|
|
4480
4588
|
);
|
|
4481
4589
|
console.log(" * Running. Press Ctrl+C to stop.\n");
|
|
4482
4590
|
await runtime.run();
|