@elisym/cli 0.31.0 → 0.32.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 +293 -79
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
import { ReadableStream } from 'node:stream/web';
|
|
3
3
|
import { readFileSync, existsSync, readdirSync, statSync, renameSync, chmodSync, mkdirSync, writeFileSync, appendFileSync, unlinkSync, rmdirSync, realpathSync } 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';
|
|
5
|
+
import { DEFAULTS, SolanaPaymentStrategy, validateAgentName, RELAYS, ElisymIdentity, splAssetsForNetwork, formatSol, formatAssetAmount, signerFromSecretKeyBase58, resolveUsdcAsset, ElisymClient, KIND_EXTERNAL_IDENTITIES, POLICY_D_TAG_PREFIX, KIND_LONG_FORM_ARTICLE, deleteControlCharacters, 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, PROVIDER_REFUSED_PREFIX, PROVIDER_FAILED_MESSAGE, NATIVE_SOL, GIST_ID_REGEX, TWEET_ID_REGEX } from '@elisym/sdk';
|
|
6
6
|
import { ElisymYamlSchema, resolveInHome, resolveInProject, createAgentDir, writeYamlInitial, writeExampleSkillTemplate, writeSecrets, listAgents, loadAgent, writeYaml, agentPaths, readMediaCache, loadPoliciesFromDir, ensureGitignoreHasIrohEntry, ensureGitignoreHasX402Entries, ensureGitignoreHasSessionsEntry, ensureGitignoreHasDelegationNoncesEntry, readAgentPublic, lookupCachedUrl, newCacheEntry, 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 { mkdir, writeFile,
|
|
12
|
-
import { parseSkillMd, validateSkillFrontmatter, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, OnchainCallSkill, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
11
|
+
import { mkdir, writeFile, mkdtemp, rm, readFile, rename, stat } from 'node:fs/promises';
|
|
12
|
+
import { parseSkillMd, validateSkillFrontmatter, isHostScratchError, isScriptRefusalError, startsWithRefusalHint, HostScratchError, DEFAULT_X402_MAX_INPUT_BYTES, resolveInsidePathReal, DEFAULT_SCRIPT_TIMEOUT_MS, OnchainCallSkill, DynamicScriptSkill as DynamicScriptSkill$1, StaticScriptSkill as StaticScriptSkill$1, StaticFileSkill as StaticFileSkill$1, ScriptSkill as ScriptSkill$1 } from '@elisym/sdk/skills';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import Decimal from 'decimal.js-light';
|
|
15
15
|
import { decodePaymentRequiredHeader } from '@x402/core/http';
|
|
16
16
|
import { createHash } from 'node:crypto';
|
|
17
|
-
import { LlmHealthMonitor, startLlmRecovery, createFreeLlmLimiterSet,
|
|
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';
|
|
@@ -3804,9 +3804,6 @@ function resolveProviderApiKey(input) {
|
|
|
3804
3804
|
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
3805
|
};
|
|
3806
3806
|
}
|
|
3807
|
-
function sanitizeForTerminal(value) {
|
|
3808
|
-
return value.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "");
|
|
3809
|
-
}
|
|
3810
3807
|
function resolveLevel(options) {
|
|
3811
3808
|
if (options.level) {
|
|
3812
3809
|
return options.level;
|
|
@@ -3845,7 +3842,7 @@ function createLogger(options = {}) {
|
|
|
3845
3842
|
logger = pino(baseOptions, pino.destination(2));
|
|
3846
3843
|
}
|
|
3847
3844
|
function logWithIndent(line) {
|
|
3848
|
-
process.stdout.write(` ${
|
|
3845
|
+
process.stdout.write(` ${deleteControlCharacters(line)}
|
|
3849
3846
|
`);
|
|
3850
3847
|
}
|
|
3851
3848
|
return {
|
|
@@ -5324,24 +5321,106 @@ function resolveJobAsset(tags, skills) {
|
|
|
5324
5321
|
const skill = skills.route(tags);
|
|
5325
5322
|
return skill?.asset ?? NATIVE_SOL;
|
|
5326
5323
|
}
|
|
5327
|
-
var BILLING_BODY_MARKERS3 = ["credit balance", "billing", "insufficient", "insufficient_quota"];
|
|
5328
5324
|
var SCRIPT_BILLING_INVALID_MARKERS = [
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5325
|
+
// Three questions, one row each, so an edit cannot land half-applied: does
|
|
5326
|
+
// this phrase gate THIS pair at all, does it also take every model on the
|
|
5327
|
+
// operator's key offline (`cascades`), and is it about money rather than
|
|
5328
|
+
// credentials (`billing`, which picks the reason the operator is shown and
|
|
5329
|
+
// the recovery probe reads).
|
|
5330
|
+
//
|
|
5331
|
+
// `cascades` is the expensive one. Gating ONE pair on a false positive costs
|
|
5332
|
+
// the operator a capability until the recovery probe clears it; a cascade
|
|
5333
|
+
// costs them every capability on that key. So it needs a phrase an unrelated
|
|
5334
|
+
// failure does not produce: `insufficient` alone is a Solana builder saying
|
|
5335
|
+
// "insufficient funds for rent" and `billing` alone is a form field, while
|
|
5336
|
+
// nothing prints `invalid x-api-key` or `credit balance` except the provider
|
|
5337
|
+
// whose key it is. The bare status words carry `cascades: false` for the same
|
|
5338
|
+
// reason - `unauthorized` is what any proxy says when it did not like a
|
|
5339
|
+
// request. The LLM path next door demands an HTTP 401/402 before cascading;
|
|
5340
|
+
// this is the script path's version of that bar.
|
|
5341
|
+
{ phrase: "credit balance", cascades: true, billing: true },
|
|
5342
|
+
{ phrase: "billing", cascades: false, billing: true },
|
|
5343
|
+
{ phrase: "insufficient", cascades: false, billing: true },
|
|
5344
|
+
{ phrase: "insufficient_quota", cascades: true, billing: true },
|
|
5345
|
+
// Gates this pair, never cascades: `x-api-key` is the name of a REQUEST
|
|
5346
|
+
// HEADER, which `curl -v` and any client that prints its own headers emit on a
|
|
5347
|
+
// perfectly ordinary failure. Taking every model on the key offline for that
|
|
5348
|
+
// is the expensive direction; `invalid x-api-key` below is the provider
|
|
5349
|
+
// actually rejecting it.
|
|
5350
|
+
{ phrase: "x-api-key", cascades: false, billing: false },
|
|
5351
|
+
// The provider REJECTING the key, which no request header says.
|
|
5352
|
+
{ phrase: "invalid x-api-key", cascades: true, billing: false },
|
|
5353
|
+
{ phrase: "invalid api key", cascades: true, billing: false },
|
|
5354
|
+
{ phrase: "invalid_api_key", cascades: true, billing: false },
|
|
5355
|
+
{ phrase: "authentication_error", cascades: true, billing: false },
|
|
5356
|
+
{ phrase: "unauthorized", cascades: false, billing: false },
|
|
5357
|
+
{ phrase: "unauthenticated", cascades: false, billing: false }
|
|
5339
5358
|
];
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5359
|
+
var BILLING_BODY_MARKERS3 = SCRIPT_BILLING_INVALID_MARKERS.filter((marker) => marker.billing).map(
|
|
5360
|
+
(marker) => marker.phrase
|
|
5361
|
+
);
|
|
5362
|
+
function classifyScriptSignal(message) {
|
|
5363
|
+
let looksBillingOrInvalid = false;
|
|
5364
|
+
let billing = false;
|
|
5365
|
+
let cascade = false;
|
|
5366
|
+
let signalAt = -1;
|
|
5367
|
+
for (const match of message.matchAll(ALL_MARKERS_RE)) {
|
|
5368
|
+
const marker = MARKER_BY_PHRASE.get(match[0].toLowerCase());
|
|
5369
|
+
looksBillingOrInvalid = true;
|
|
5370
|
+
billing = billing || marker?.billing === true;
|
|
5371
|
+
cascade = cascade || marker?.cascades === true;
|
|
5372
|
+
if (signalAt === -1) {
|
|
5373
|
+
signalAt = match.index;
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
return { looksBillingOrInvalid, reason: billing ? "billing" : "invalid", cascade, signalAt };
|
|
5377
|
+
}
|
|
5378
|
+
var HOST_DISK_CODES = /* @__PURE__ */ new Set(["ENOSPC", "EROFS", "EDQUOT", "EMFILE", "ENFILE", "EIO"]);
|
|
5379
|
+
function asHostScratchFailure(error) {
|
|
5380
|
+
const code = error?.code;
|
|
5381
|
+
if (code === void 0 || !HOST_DISK_CODES.has(code)) {
|
|
5382
|
+
return void 0;
|
|
5383
|
+
}
|
|
5384
|
+
return new HostScratchError(error instanceof Error ? error.message : String(error));
|
|
5385
|
+
}
|
|
5386
|
+
var HEALTH_REASON_CHARS = 500;
|
|
5387
|
+
var SIGNAL_LEAD_UNITS = 80;
|
|
5388
|
+
function markerPattern(phrases, flags) {
|
|
5389
|
+
const longestFirst = [...phrases].sort((left, right) => right.length - left.length);
|
|
5390
|
+
return new RegExp(
|
|
5391
|
+
longestFirst.map((phrase) => phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"),
|
|
5392
|
+
flags
|
|
5393
|
+
);
|
|
5394
|
+
}
|
|
5395
|
+
var ALL_MARKERS_RE = markerPattern(
|
|
5396
|
+
SCRIPT_BILLING_INVALID_MARKERS.map((marker) => marker.phrase),
|
|
5397
|
+
"gi"
|
|
5398
|
+
);
|
|
5399
|
+
var MARKER_BY_PHRASE = new Map(
|
|
5400
|
+
SCRIPT_BILLING_INVALID_MARKERS.map((marker) => [marker.phrase, marker])
|
|
5401
|
+
);
|
|
5402
|
+
function signalIndex(text) {
|
|
5403
|
+
ALL_MARKERS_RE.lastIndex = 0;
|
|
5404
|
+
const found = ALL_MARKERS_RE.exec(text)?.index ?? -1;
|
|
5405
|
+
ALL_MARKERS_RE.lastIndex = 0;
|
|
5406
|
+
return found;
|
|
5407
|
+
}
|
|
5408
|
+
function operatorReason(diagnostic, known, budget = HEALTH_REASON_CHARS) {
|
|
5409
|
+
if (startsWithRefusalHint(diagnostic)) {
|
|
5410
|
+
return excerptUntrusted(diagnostic, budget);
|
|
5411
|
+
}
|
|
5412
|
+
const signalAt = known ?? signalIndex(diagnostic);
|
|
5413
|
+
if (signalAt === -1) {
|
|
5414
|
+
return excerptUntrustedTail(diagnostic, budget);
|
|
5415
|
+
}
|
|
5416
|
+
const from = Math.max(0, signalAt - SIGNAL_LEAD_UNITS);
|
|
5417
|
+
if (from === 0) {
|
|
5418
|
+
return excerptUntrusted(diagnostic, budget);
|
|
5419
|
+
}
|
|
5420
|
+
return `\u2026${excerptUntrusted(diagnostic.slice(from, from + budget * 8), budget - 1)}`;
|
|
5343
5421
|
}
|
|
5344
5422
|
var AGENT_UNAVAILABLE_MESSAGE = "Agent temporarily unavailable";
|
|
5423
|
+
var OPERATOR_EXCERPT_CHARS = 1500;
|
|
5345
5424
|
var AgentUnavailableError = class extends Error {
|
|
5346
5425
|
constructor() {
|
|
5347
5426
|
super(AGENT_UNAVAILABLE_MESSAGE);
|
|
@@ -5395,16 +5474,49 @@ function customerSafeMessage(error) {
|
|
|
5395
5474
|
if (error instanceof AgentUnavailableError || error instanceof ExecutionBudgetExceededError || error instanceof SeedFailedError) {
|
|
5396
5475
|
return error.message;
|
|
5397
5476
|
}
|
|
5398
|
-
if (error
|
|
5399
|
-
return error.message
|
|
5477
|
+
if (isScriptRefusalError(error)) {
|
|
5478
|
+
return `${PROVIDER_REFUSED_PREFIX}${error.message}`;
|
|
5400
5479
|
}
|
|
5401
|
-
if (error
|
|
5480
|
+
if (isScriptExecutionError(error)) {
|
|
5481
|
+
return PROVIDER_FAILED_MESSAGE;
|
|
5482
|
+
}
|
|
5483
|
+
if (isScriptBillingExhaustedError(error) || isHostScratchError(error) || error instanceof X402TransientError) {
|
|
5402
5484
|
return AGENT_UNAVAILABLE_MESSAGE;
|
|
5403
5485
|
}
|
|
5404
5486
|
if (error instanceof Error && CUSTOMER_SAFE_MESSAGE_PREFIXES.some((prefix) => error.message.startsWith(prefix))) {
|
|
5405
5487
|
return error.message;
|
|
5406
5488
|
}
|
|
5407
|
-
return
|
|
5489
|
+
return PROVIDER_FAILED_MESSAGE;
|
|
5490
|
+
}
|
|
5491
|
+
function needsScratchSpace(mode, hasAttachment) {
|
|
5492
|
+
if (hasAttachment) {
|
|
5493
|
+
return true;
|
|
5494
|
+
}
|
|
5495
|
+
return mode !== void 0 && mode !== "llm" && mode !== "static-file" && mode !== "static-script";
|
|
5496
|
+
}
|
|
5497
|
+
function describeForOperator(error) {
|
|
5498
|
+
if (isHostScratchError(error)) {
|
|
5499
|
+
return `${error.message}: ${excerptUntrusted(error.detail, OPERATOR_EXCERPT_CHARS)}`;
|
|
5500
|
+
}
|
|
5501
|
+
if (isScriptBillingExhaustedError(error)) {
|
|
5502
|
+
return `script signalled billing exhausted: ${excerptUntrustedTail(scriptOutput(error), OPERATOR_EXCERPT_CHARS)}`;
|
|
5503
|
+
}
|
|
5504
|
+
if (isScriptRefusalError(error)) {
|
|
5505
|
+
return error.stderr === "" ? `refused: ${error.message}` : `refused: ${error.message} (stderr: ${error.stderr})`;
|
|
5506
|
+
}
|
|
5507
|
+
if (isScriptExecutionError(error)) {
|
|
5508
|
+
return `${error.message}: ${excerptUntrustedTail(error.detail, OPERATOR_EXCERPT_CHARS)}`;
|
|
5509
|
+
}
|
|
5510
|
+
if (typeof error === "string") {
|
|
5511
|
+
return excerptUntrusted(error, OPERATOR_EXCERPT_CHARS);
|
|
5512
|
+
}
|
|
5513
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
5514
|
+
const message = error.message;
|
|
5515
|
+
if (typeof message === "string" && message !== "") {
|
|
5516
|
+
return excerptUntrusted(message, OPERATOR_EXCERPT_CHARS);
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
return "Unknown error";
|
|
5408
5520
|
}
|
|
5409
5521
|
function parseRawEventDelegated(rawEventJson) {
|
|
5410
5522
|
if (rawEventJson === void 0) {
|
|
@@ -5691,12 +5803,51 @@ var AgentRuntime = class {
|
|
|
5691
5803
|
}
|
|
5692
5804
|
return ` (cascading to ${siblings} other model(s) for ${provider} sharing the same API key)`;
|
|
5693
5805
|
}
|
|
5806
|
+
/**
|
|
5807
|
+
* Whether this agent can still make itself a scratch directory.
|
|
5808
|
+
*
|
|
5809
|
+
* A `HostScratchError` says the last job could not run because the temp
|
|
5810
|
+
* directory refused it. That is not the operator's API key, so it must not
|
|
5811
|
+
* gate the health pair - but SOMETHING has to stop the next customer paying
|
|
5812
|
+
* into an agent that will fail them the same way, which is what the health
|
|
5813
|
+
* gate used to do by accident. Probed before payment, and only once such a
|
|
5814
|
+
* failure has been seen - a working host pays nothing for this, and a broken
|
|
5815
|
+
* one is asked again on every job rather than after a timer: an agent whose
|
|
5816
|
+
* jobs arrive minutes apart would otherwise let every customer through.
|
|
5817
|
+
*/
|
|
5818
|
+
scratchFailed = false;
|
|
5819
|
+
async scratchSpaceUsable() {
|
|
5820
|
+
if (!this.scratchFailed) {
|
|
5821
|
+
return true;
|
|
5822
|
+
}
|
|
5823
|
+
const probe = await mkdtemp(join(tmpdir(), "elisym-probe-")).catch(() => null);
|
|
5824
|
+
if (probe === null) {
|
|
5825
|
+
return false;
|
|
5826
|
+
}
|
|
5827
|
+
const wrote = await writeFile(join(probe, "probe"), "x").then(() => true).catch(() => false);
|
|
5828
|
+
await rm(probe, { recursive: true, force: true }).catch(() => {
|
|
5829
|
+
});
|
|
5830
|
+
if (!wrote) {
|
|
5831
|
+
return false;
|
|
5832
|
+
}
|
|
5833
|
+
this.scratchFailed = false;
|
|
5834
|
+
return true;
|
|
5835
|
+
}
|
|
5694
5836
|
markHealthFromExecuteError(skill, err, log, jobId) {
|
|
5695
5837
|
if (!this.healthMonitor) {
|
|
5696
5838
|
return false;
|
|
5697
5839
|
}
|
|
5698
5840
|
const tag = `[${jobId.slice(0, 8)}]`;
|
|
5699
|
-
if (err
|
|
5841
|
+
if (isHostScratchError(err)) {
|
|
5842
|
+
log(
|
|
5843
|
+
`${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.`
|
|
5844
|
+
);
|
|
5845
|
+
return false;
|
|
5846
|
+
}
|
|
5847
|
+
if (isScriptRefusalError(err)) {
|
|
5848
|
+
return false;
|
|
5849
|
+
}
|
|
5850
|
+
if (isScriptBillingExhaustedError(err)) {
|
|
5700
5851
|
const provider = skill.llmOverride?.provider;
|
|
5701
5852
|
const model = skill.llmOverride?.model;
|
|
5702
5853
|
if (!provider || !model) {
|
|
@@ -5708,7 +5859,12 @@ var AgentRuntime = class {
|
|
|
5708
5859
|
log(
|
|
5709
5860
|
`${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
5861
|
);
|
|
5711
|
-
this.healthMonitor.markUnhealthyFromJob(
|
|
5862
|
+
this.healthMonitor.markUnhealthyFromJob(
|
|
5863
|
+
provider,
|
|
5864
|
+
model,
|
|
5865
|
+
"billing",
|
|
5866
|
+
excerptUntrustedTail(scriptOutput(err), 200)
|
|
5867
|
+
);
|
|
5712
5868
|
return true;
|
|
5713
5869
|
}
|
|
5714
5870
|
if (skill.mode === "llm" && skill.resolvedTriple) {
|
|
@@ -5718,7 +5874,7 @@ var AgentRuntime = class {
|
|
|
5718
5874
|
return false;
|
|
5719
5875
|
}
|
|
5720
5876
|
const status = Number(match[1]);
|
|
5721
|
-
const body = (match[2] ?? ""
|
|
5877
|
+
const body = excerptUntrusted(match[2] ?? "", 200);
|
|
5722
5878
|
const isBillingStatus = status === 402;
|
|
5723
5879
|
const isAuthStatus = status === 401 || status === 403;
|
|
5724
5880
|
const isBilling400 = status === 400 && bodyLooksLikeBilling3(body);
|
|
@@ -5737,33 +5893,43 @@ var AgentRuntime = class {
|
|
|
5737
5893
|
}
|
|
5738
5894
|
if (skill.mode !== "llm") {
|
|
5739
5895
|
let message;
|
|
5740
|
-
|
|
5741
|
-
|
|
5896
|
+
let scanned;
|
|
5897
|
+
let diagnostic;
|
|
5898
|
+
if (isScriptExecutionError(err)) {
|
|
5899
|
+
message = err.stderr ?? err.detail;
|
|
5900
|
+
diagnostic = startsWithRefusalHint(err.detail) || message.trim() === "" ? err.detail : message;
|
|
5901
|
+
scanned = err.stderr === void 0 ? "the skill error" : "stderr";
|
|
5742
5902
|
} else if (err instanceof Error) {
|
|
5743
5903
|
message = err.message;
|
|
5904
|
+
diagnostic = message;
|
|
5905
|
+
scanned = "the skill error";
|
|
5744
5906
|
} else {
|
|
5745
5907
|
message = String(err);
|
|
5908
|
+
diagnostic = message;
|
|
5909
|
+
scanned = "the skill error";
|
|
5746
5910
|
}
|
|
5747
5911
|
const provider = skill.llmOverride?.provider;
|
|
5748
5912
|
const model = skill.llmOverride?.model;
|
|
5749
5913
|
if (!provider || !model) {
|
|
5750
5914
|
log(
|
|
5751
|
-
`${tag} Script "${skill.name}" failed ("${
|
|
5915
|
+
`${tag} Script "${skill.name}" failed ("${operatorReason(diagnostic)}") but did not declare provider/model in SKILL.md - cannot gate future jobs.`
|
|
5752
5916
|
);
|
|
5753
5917
|
return false;
|
|
5754
5918
|
}
|
|
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;
|
|
5919
|
+
const { looksBillingOrInvalid, reason, cascade, signalAt } = classifyScriptSignal(message);
|
|
5759
5920
|
const cascadeNote = cascade ? this.cascadeSuffix(provider, model) : " (no cascade)";
|
|
5760
|
-
const signalNote = looksBillingOrInvalid ? `${reason} signal in
|
|
5921
|
+
const signalNote = looksBillingOrInvalid ? `${reason} signal in ${scanned}` : `generic exit (no billing/invalid markers, classified as ${reason}, skill-local)`;
|
|
5761
5922
|
log(
|
|
5762
5923
|
`${tag} Script failure (${signalNote}). Marking ${provider}/${model} unhealthy${cascadeNote}; future jobs against this pair will be refused until recovery probe succeeds.`
|
|
5763
5924
|
);
|
|
5764
|
-
this.healthMonitor.markUnhealthyFromJob(
|
|
5765
|
-
|
|
5766
|
-
|
|
5925
|
+
this.healthMonitor.markUnhealthyFromJob(
|
|
5926
|
+
provider,
|
|
5927
|
+
model,
|
|
5928
|
+
reason,
|
|
5929
|
+
// The index only when the quote is of the same string the scan read.
|
|
5930
|
+
operatorReason(diagnostic, diagnostic === message ? signalAt : void 0),
|
|
5931
|
+
{ cascade }
|
|
5932
|
+
);
|
|
5767
5933
|
return true;
|
|
5768
5934
|
}
|
|
5769
5935
|
return false;
|
|
@@ -5878,8 +6044,8 @@ var AgentRuntime = class {
|
|
|
5878
6044
|
if (jobSession) {
|
|
5879
6045
|
jobSession.store.admitLive(jobSession.customerId, jobSession.sessionId);
|
|
5880
6046
|
}
|
|
5881
|
-
this.limit(() => this.processJob(job)).catch((
|
|
5882
|
-
this.callbacks.onJobError?.(job.jobId,
|
|
6047
|
+
this.limit(() => this.processJob(job)).catch((err) => {
|
|
6048
|
+
this.callbacks.onJobError?.(job.jobId, describeForOperator(err));
|
|
5883
6049
|
}).finally(() => {
|
|
5884
6050
|
this.inFlight.delete(job.jobId);
|
|
5885
6051
|
this.pending--;
|
|
@@ -5964,18 +6130,31 @@ var AgentRuntime = class {
|
|
|
5964
6130
|
this.jobAbortControllers.add(jobAbort);
|
|
5965
6131
|
try {
|
|
5966
6132
|
await this.executeJob(job, jobAbort.signal);
|
|
5967
|
-
} catch (
|
|
6133
|
+
} catch (raw) {
|
|
6134
|
+
const e = asHostScratchFailure(raw) ?? raw;
|
|
5968
6135
|
const log = this.callbacks.onLog ?? console.log;
|
|
5969
|
-
|
|
6136
|
+
const operatorMessage = describeForOperator(e);
|
|
6137
|
+
log(`[${job.jobId.slice(0, 8)}] Error: ${operatorMessage}`);
|
|
6138
|
+
if (isHostScratchError(e)) {
|
|
6139
|
+
this.scratchFailed = true;
|
|
6140
|
+
}
|
|
5970
6141
|
const currentStatus = this.ledger.getStatus(job.jobId);
|
|
5971
|
-
const keepPaidForRecovery = (e instanceof AgentUnavailableError ||
|
|
6142
|
+
const keepPaidForRecovery = (e instanceof AgentUnavailableError || // The exit-42 contract: the key is out of credits, not the job out of
|
|
6143
|
+
// sense, so the job waits for the operator to top up. Reached when the
|
|
6144
|
+
// gate did NOT flip - an agent with no health monitor, or a skill that
|
|
6145
|
+
// declares no pair - where the customer is told "temporarily
|
|
6146
|
+
// unavailable" and that has to stay true of their money.
|
|
6147
|
+
isScriptBillingExhaustedError(e) || // The agent's own disk, not the job: a tmpdir that is full or
|
|
6148
|
+
// read-only now may not be in five minutes, and the customer has
|
|
6149
|
+
// already paid. Terminating here would keep their money for a failure
|
|
6150
|
+
// that was never about their request.
|
|
6151
|
+
isHostScratchError(e) || e instanceof SeedFailedError || e instanceof PaymentTimeoutError || e instanceof X402TransientError || e instanceof ExecutionBudgetExceededError && this.skills.route(job.tags)?.mode === "x402") && currentStatus === "paid";
|
|
5972
6152
|
if (currentStatus !== "executed" && !keepPaidForRecovery) {
|
|
5973
6153
|
this.ledger.markFailed(job.jobId);
|
|
5974
6154
|
}
|
|
5975
6155
|
if (keepPaidForRecovery) {
|
|
5976
6156
|
log(`[${job.jobId.slice(0, 8)}] Keeping status=paid; recovery will retry (24h cutoff).`);
|
|
5977
6157
|
}
|
|
5978
|
-
const operatorMessage = e instanceof ScriptExecutionError ? `${e.message}: ${e.detail}` : e.message ?? "Unknown error";
|
|
5979
6158
|
this.callbacks.onJobError?.(job.jobId, operatorMessage);
|
|
5980
6159
|
const safeMessage = customerSafeMessage(e);
|
|
5981
6160
|
await this.transport.sendFeedback(job, { type: "error", message: safeMessage }).catch(() => {
|
|
@@ -6007,6 +6186,14 @@ var AgentRuntime = class {
|
|
|
6007
6186
|
return;
|
|
6008
6187
|
}
|
|
6009
6188
|
}
|
|
6189
|
+
if (needsScratchSpace(matched?.mode, job.attachment !== void 0) && !await this.scratchSpaceUsable()) {
|
|
6190
|
+
log(
|
|
6191
|
+
`[${job.jobId.slice(0, 8)}] Refusing job before payment: this agent cannot create scratch space (check the temp directory).`
|
|
6192
|
+
);
|
|
6193
|
+
await this.transport.sendFeedback(job, { type: "error", message: AGENT_UNAVAILABLE_MESSAGE }).catch(() => {
|
|
6194
|
+
});
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
6010
6197
|
if (matched?.mode === "x402" && job.attachment !== void 0) {
|
|
6011
6198
|
const method = matched.x402?.method ?? "POST";
|
|
6012
6199
|
const maxInputBytes = matched.x402?.maxInputBytes ?? 0;
|
|
@@ -6893,7 +7080,9 @@ var AgentRuntime = class {
|
|
|
6893
7080
|
});
|
|
6894
7081
|
return this.materializeBytesInput(bytes, attachment.mime);
|
|
6895
7082
|
}
|
|
6896
|
-
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"))
|
|
7083
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-")).catch((err) => {
|
|
7084
|
+
throw new HostScratchError(err instanceof Error ? err.message : String(err));
|
|
7085
|
+
});
|
|
6897
7086
|
const filePath = join(dir, "input");
|
|
6898
7087
|
try {
|
|
6899
7088
|
await this.irohTransport.fetchToPath(irohMember.ticket, filePath, {
|
|
@@ -6945,14 +7134,16 @@ var AgentRuntime = class {
|
|
|
6945
7134
|
return { inlineText: Buffer.from(bytes).toString("utf8"), cleanup: async () => {
|
|
6946
7135
|
} };
|
|
6947
7136
|
}
|
|
6948
|
-
const dir = await mkdtemp(join(tmpdir(), "elisym-job-"))
|
|
7137
|
+
const dir = await mkdtemp(join(tmpdir(), "elisym-job-")).catch((err) => {
|
|
7138
|
+
throw new HostScratchError(err instanceof Error ? err.message : String(err));
|
|
7139
|
+
});
|
|
6949
7140
|
const filePath = join(dir, "input");
|
|
6950
7141
|
try {
|
|
6951
7142
|
await writeFile(filePath, bytes);
|
|
6952
7143
|
} catch (error) {
|
|
6953
7144
|
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
6954
7145
|
});
|
|
6955
|
-
throw error;
|
|
7146
|
+
throw asHostScratchFailure(error) ?? error;
|
|
6956
7147
|
}
|
|
6957
7148
|
return {
|
|
6958
7149
|
filePath,
|
|
@@ -7287,12 +7478,13 @@ var AgentRuntime = class {
|
|
|
7287
7478
|
this.limit(async () => {
|
|
7288
7479
|
try {
|
|
7289
7480
|
await this.recoverSingleJob(entry, log);
|
|
7290
|
-
} catch (
|
|
7291
|
-
log(`[${entry.job_id.slice(0, 8)}] Recovery: failed: ${
|
|
7481
|
+
} catch (err) {
|
|
7482
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: failed: ${describeForOperator(err)}`);
|
|
7292
7483
|
} finally {
|
|
7293
7484
|
this.inFlight.delete(entry.job_id);
|
|
7294
7485
|
this.pending--;
|
|
7295
7486
|
}
|
|
7487
|
+
}).catch(() => {
|
|
7296
7488
|
});
|
|
7297
7489
|
}
|
|
7298
7490
|
}
|
|
@@ -7398,7 +7590,7 @@ var AgentRuntime = class {
|
|
|
7398
7590
|
try {
|
|
7399
7591
|
await this.healthMonitor.assertReady(healthPair.provider, healthPair.model);
|
|
7400
7592
|
} catch (err) {
|
|
7401
|
-
if (err
|
|
7593
|
+
if (isLlmHealthError(err)) {
|
|
7402
7594
|
log(
|
|
7403
7595
|
`[${entry.job_id.slice(0, 8)}] Recovery: pair ${healthPair.provider}/${healthPair.model} still unhealthy (${err.reason}); waiting for recovery probe.`
|
|
7404
7596
|
);
|
|
@@ -7442,7 +7634,15 @@ var AgentRuntime = class {
|
|
|
7442
7634
|
entry.customer_id,
|
|
7443
7635
|
recoveryAbort.signal
|
|
7444
7636
|
);
|
|
7445
|
-
} catch {
|
|
7637
|
+
} catch (raw) {
|
|
7638
|
+
const err = asHostScratchFailure(raw) ?? raw;
|
|
7639
|
+
if (isHostScratchError(err)) {
|
|
7640
|
+
this.scratchFailed = true;
|
|
7641
|
+
log(
|
|
7642
|
+
`[${entry.job_id.slice(0, 8)}] Recovery: no scratch space for the input file; leaving the job paid.`
|
|
7643
|
+
);
|
|
7644
|
+
return;
|
|
7645
|
+
}
|
|
7446
7646
|
log(`[${entry.job_id.slice(0, 8)}] Recovery: input file unavailable, marking failed`);
|
|
7447
7647
|
await this.failRecoveredJob(entry, fakeJob, RECOVERY_INPUT_UNAVAILABLE_CUSTOMER_MESSAGE);
|
|
7448
7648
|
return;
|
|
@@ -7453,6 +7653,7 @@ var AgentRuntime = class {
|
|
|
7453
7653
|
skill
|
|
7454
7654
|
);
|
|
7455
7655
|
const runRecoveryExecution = async (history) => {
|
|
7656
|
+
let budgetExceeded = false;
|
|
7456
7657
|
let budgetTimer;
|
|
7457
7658
|
try {
|
|
7458
7659
|
const execPromise = skill.execute(
|
|
@@ -7474,6 +7675,7 @@ var AgentRuntime = class {
|
|
|
7474
7675
|
execPromise,
|
|
7475
7676
|
new Promise((_resolve, reject) => {
|
|
7476
7677
|
budgetTimer = setTimeout(() => {
|
|
7678
|
+
budgetExceeded = true;
|
|
7477
7679
|
recoveryAbort.abort();
|
|
7478
7680
|
reject(new ExecutionBudgetExceededError(recoveryBudgetMs));
|
|
7479
7681
|
}, recoveryBudgetMs);
|
|
@@ -7482,6 +7684,12 @@ var AgentRuntime = class {
|
|
|
7482
7684
|
}
|
|
7483
7685
|
return await execPromise;
|
|
7484
7686
|
} catch (err) {
|
|
7687
|
+
if (isHostScratchError(err)) {
|
|
7688
|
+
this.scratchFailed = true;
|
|
7689
|
+
}
|
|
7690
|
+
if (budgetExceeded) {
|
|
7691
|
+
throw new ExecutionBudgetExceededError(recoveryBudgetMs);
|
|
7692
|
+
}
|
|
7485
7693
|
this.markHealthFromExecuteError(skill, err, log, entry.job_id);
|
|
7486
7694
|
throw err;
|
|
7487
7695
|
} finally {
|
|
@@ -7494,18 +7702,28 @@ var AgentRuntime = class {
|
|
|
7494
7702
|
}
|
|
7495
7703
|
}
|
|
7496
7704
|
};
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7705
|
+
let output;
|
|
7706
|
+
try {
|
|
7707
|
+
output = recoveryJobSession === null ? await runRecoveryExecution() : await this.runSessionExchange({
|
|
7708
|
+
session: recoveryJobSession,
|
|
7709
|
+
jobId: entry.job_id,
|
|
7710
|
+
userRecord: buildUserRecord(
|
|
7711
|
+
recoveryInputFile?.inlineText ?? entry.input,
|
|
7712
|
+
recoveryInputFile?.filePath !== void 0 ? "attachment" : void 0
|
|
7713
|
+
),
|
|
7714
|
+
signal: recoveryAbort.signal,
|
|
7715
|
+
log,
|
|
7716
|
+
execute: runRecoveryExecution,
|
|
7717
|
+
excludeOwnTurns: true
|
|
7718
|
+
});
|
|
7719
|
+
} catch (error) {
|
|
7720
|
+
if (!isScriptRefusalError(error)) {
|
|
7721
|
+
throw error;
|
|
7722
|
+
}
|
|
7723
|
+
log(`[${entry.job_id.slice(0, 8)}] Recovery: ${describeForOperator(error)}`);
|
|
7724
|
+
await this.failRecoveredJob(entry, fakeJob, customerSafeMessage(error));
|
|
7725
|
+
return;
|
|
7726
|
+
}
|
|
7509
7727
|
const { attachments: resultAttachments, deliveredContent } = await this.buildResultAttachment(
|
|
7510
7728
|
entry.job_id,
|
|
7511
7729
|
output,
|
|
@@ -8731,21 +8949,17 @@ function withPaymentIdentifier(headerValue, declaration, jobId) {
|
|
|
8731
8949
|
return headerValue;
|
|
8732
8950
|
}
|
|
8733
8951
|
}
|
|
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
8952
|
function printedPrefix(flattened) {
|
|
8739
|
-
return flattened
|
|
8953
|
+
return clipToCodeUnits(flattened, X402_ERROR_EXCERPT_CHARS);
|
|
8740
8954
|
}
|
|
8741
8955
|
function clipForOperator(flattened) {
|
|
8742
8956
|
return flattened.length <= X402_ERROR_EXCERPT_CHARS ? flattened : `${printedPrefix(flattened)}...`;
|
|
8743
8957
|
}
|
|
8744
8958
|
function quoteUpstream(text) {
|
|
8745
|
-
return clipForOperator(
|
|
8959
|
+
return clipForOperator(flattenForComparison(text));
|
|
8746
8960
|
}
|
|
8747
8961
|
function quoteUpstreamWithInput(text, sentInput) {
|
|
8748
|
-
return clipForOperator(maskCustomerInput(
|
|
8962
|
+
return clipForOperator(maskCustomerInput(flattenForComparison(text), inputForms(sentInput)));
|
|
8749
8963
|
}
|
|
8750
8964
|
function wireForm(sentInput) {
|
|
8751
8965
|
return new TextDecoder().decode(new TextEncoder().encode(sentInput));
|
|
@@ -8767,7 +8981,7 @@ function endsInsideInput(text, forms) {
|
|
|
8767
8981
|
}
|
|
8768
8982
|
function inputForms(sentInput) {
|
|
8769
8983
|
return [wireForm(sentInput), jsonEscapedForm(sentInput), ...urlEncodedForms(sentInput)].map(
|
|
8770
|
-
|
|
8984
|
+
flattenForComparison
|
|
8771
8985
|
);
|
|
8772
8986
|
}
|
|
8773
8987
|
function maskCustomerInput(text, forms) {
|
|
@@ -8811,7 +9025,7 @@ var X402Driver = class {
|
|
|
8811
9025
|
signerPromise = null;
|
|
8812
9026
|
repriceHintLogged = /* @__PURE__ */ new Set();
|
|
8813
9027
|
log(message) {
|
|
8814
|
-
(this.options.log ?? console.log)(`[x402] ${
|
|
9028
|
+
(this.options.log ?? console.log)(`[x402] ${flattenForComparison(message)}`);
|
|
8815
9029
|
}
|
|
8816
9030
|
getSigner() {
|
|
8817
9031
|
const secret = this.options.solanaSecretKeyBase58;
|
|
@@ -9015,7 +9229,7 @@ var X402Driver = class {
|
|
|
9015
9229
|
this.options.errorBodyReadMs ?? X402_ERROR_BODY_READ_MS
|
|
9016
9230
|
);
|
|
9017
9231
|
const forms = inputForms(sentInput);
|
|
9018
|
-
const quoted = maskCustomerInput(
|
|
9232
|
+
const quoted = maskCustomerInput(flattenForComparison(body.text), forms);
|
|
9019
9233
|
if (quoted.length === 0) {
|
|
9020
9234
|
return "";
|
|
9021
9235
|
}
|
|
@@ -9667,7 +9881,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9667
9881
|
}
|
|
9668
9882
|
try {
|
|
9669
9883
|
await client.policies.deletePolicy(identity2, type);
|
|
9670
|
-
console.log(` Removed stale policy: ${
|
|
9884
|
+
console.log(` Removed stale policy: ${deleteControlCharacters(type)}`);
|
|
9671
9885
|
} catch {
|
|
9672
9886
|
}
|
|
9673
9887
|
}
|
|
@@ -9778,7 +9992,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9778
9992
|
const card = JSON.parse(ev.content);
|
|
9779
9993
|
if (card.name && toDTag(card.name) === dTag) {
|
|
9780
9994
|
await client.discovery.deleteCapability(identity2, card.name);
|
|
9781
|
-
console.log(` Removed stale capability: ${
|
|
9995
|
+
console.log(` Removed stale capability: ${deleteControlCharacters(card.name)}`);
|
|
9782
9996
|
}
|
|
9783
9997
|
} catch {
|
|
9784
9998
|
}
|
|
@@ -9871,7 +10085,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9871
10085
|
ledger,
|
|
9872
10086
|
{
|
|
9873
10087
|
onJobReceived: (job) => {
|
|
9874
|
-
const cap =
|
|
10088
|
+
const cap = deleteControlCharacters(job.tags.find((t) => t !== "elisym") ?? "unknown");
|
|
9875
10089
|
process.stdout.write(` [job] ${job.jobId.slice(0, 16)} | cap=${cap}
|
|
9876
10090
|
`);
|
|
9877
10091
|
logger.info({ event: "job_received", jobId: job.jobId, capability: cap });
|
|
@@ -9882,7 +10096,7 @@ async function cmdStart(nameArg, options = {}) {
|
|
|
9882
10096
|
logger.info({ event: "job_delivered", jobId });
|
|
9883
10097
|
},
|
|
9884
10098
|
onJobError: (jobId, error) => {
|
|
9885
|
-
const safeError =
|
|
10099
|
+
const safeError = deleteControlCharacters(error);
|
|
9886
10100
|
process.stderr.write(` [job] ${jobId.slice(0, 16)} | error: ${safeError}
|
|
9887
10101
|
`);
|
|
9888
10102
|
logger.error({ event: "job_error", jobId, error: safeError });
|