@vultisig/cli 2.17.0 → 2.18.6
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/CHANGELOG.md +53 -0
- package/dist/index.js +248 -284
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# @vultisig/cli
|
|
2
2
|
|
|
3
|
+
## 2.18.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1003](https://github.com/vultisig/vultisig-sdk/pull/1003) [`b27786d`](https://github.com/vultisig/vultisig-sdk/commit/b27786d8ac596ea3d2d4a13da958b266f589b73c) Thanks [@neavra](https://github.com/neavra)! - fix(agent): sign purely from the `tool-output-available` channel and remove the `tx_ready` signing path ([#927](https://github.com/vultisig/vultisig-sdk/issues/927) Phase 2). The client-enriched tool-output candidate (flat builders and `execute_*` prep) is now the sole signing source, matching what the production backend emits — it writes the signable payload on tool-output and emits `data-tx_ready` only as a hollow `{typed_confirm}` marker the CLI never consumed. Removes the Phase-1 dual-read + parity cross-check machinery, the tx_ready capture/selection, and the recovered-tx_ready replay. Fail-closed postures are preserved (a structurally-unsignable candidate is never buffered), and a disconnect that ran a signable tool now warns to re-run rather than signing. Patch: no CLI API change and the same real transactions still sign — this aligns the internal signing source with production.
|
|
8
|
+
|
|
9
|
+
- [#1024](https://github.com/vultisig/vultisig-sdk/pull/1024) [`3bc7904`](https://github.com/vultisig/vultisig-sdk/commit/3bc790403483dd7e90dac2efc33d7bc64c18b921) Thanks [@neavra](https://github.com/neavra)! - Stop `tx-status` from reporting malformed or never-seen transaction hashes as `pending` forever.
|
|
10
|
+
|
|
11
|
+
- The EVM status resolver now distinguishes a genuinely-pending tx (the node knows the hash, receipt still lagging) from one the node has never seen, returning a new terminal `not_found` status for the latter instead of an indefinite `pending`.
|
|
12
|
+
- New `isValidTxHash(chain, hash)` helper validates a hash's shape per chain-kind; the CLI `tx-status` command validates `--tx-hash` before any RPC and fails fast with `INVALID_INPUT` (exit 4) on a malformed hash.
|
|
13
|
+
- CLI `tx-status` polling is now bounded by a total wait budget (`--timeout <seconds>`, default 120) and exits non-zero on give-up — `TX_NOT_FOUND` (exit 5) when the node has no record of the hash, `TX_STATUS_TIMEOUT` (exit 3, retryable) when it is still pending.
|
|
14
|
+
- The poll loop now caps each sleep at the remaining wait budget instead of always sleeping the full poll interval, so a small `--timeout` gives up promptly instead of overshooting by up to one poll interval.
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [[`ce38186`](https://github.com/vultisig/vultisig-sdk/commit/ce381864b977b19668702eae6e1ecad63ecbdf2b), [`3bc7904`](https://github.com/vultisig/vultisig-sdk/commit/3bc790403483dd7e90dac2efc33d7bc64c18b921)]:
|
|
17
|
+
- @vultisig/sdk@2.18.6
|
|
18
|
+
- @vultisig/core-chain@2.23.3
|
|
19
|
+
|
|
20
|
+
## 2.18.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- [#1004](https://github.com/vultisig/vultisig-sdk/pull/1004) [`42fe389`](https://github.com/vultisig/vultisig-sdk/commit/42fe3893196e7d8945f852695387828eb49bfb41) Thanks [@neavra](https://github.com/neavra)! - feat(cli): surface a typed turn-outcome for `agent ask` (success / blocked / refusal / error)
|
|
25
|
+
|
|
26
|
+
The CLI now advertises the `turn_outcome` surface and parses the backend's
|
|
27
|
+
`data-turn_outcome` SSE part, so a headless `agent ask` caller can tell four turn
|
|
28
|
+
endings apart WITHOUT parsing prose:
|
|
29
|
+
|
|
30
|
+
- **success** — the turn completed normally
|
|
31
|
+
- **blocked** — a fund-safety guardrail deliberately blocked the requested action
|
|
32
|
+
- **refusal** — the model refused or asked a clarifying question (no action taken)
|
|
33
|
+
- **error** — an infrastructure error ended the turn
|
|
34
|
+
|
|
35
|
+
Two additive surfaces:
|
|
36
|
+
|
|
37
|
+
- `agent ask --output json` gains a top-level `outcome` field
|
|
38
|
+
(`{ kind, code?, detail? }`) on both the success and error envelopes. Human
|
|
39
|
+
output prints a greppable `outcome:<kind>[:<code>]` line for non-success turns.
|
|
40
|
+
- New dedicated exit codes (additive; the 0–9 taxonomy is unchanged):
|
|
41
|
+
`10` = a fund-safety block, `11` = a refusal/clarifying question. A frame-less
|
|
42
|
+
infrastructure error now exits `1` instead of a false `0`. Success stays `0`.
|
|
43
|
+
|
|
44
|
+
Behavior change to note: a turn the backend BLOCKS or the model REFUSES previously
|
|
45
|
+
exited `0` (indistinguishable from success); it now exits `10`/`11`. Scripts that
|
|
46
|
+
treated every `agent ask` as success-on-0 should branch on `outcome.kind` (or the
|
|
47
|
+
exit code). Against an older backend that does not emit `data-turn_outcome`, the
|
|
48
|
+
`outcome` field is absent and exit codes are unchanged.
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- Updated dependencies [[`31814c4`](https://github.com/vultisig/vultisig-sdk/commit/31814c4b452ef5b2628dc12bc46e427fcf0b7237), [`b37dc3c`](https://github.com/vultisig/vultisig-sdk/commit/b37dc3c31d6f93af982cd1e082e0b4d1382dc19d), [`01c5630`](https://github.com/vultisig/vultisig-sdk/commit/01c56303d0b872a76de07da9685331106f586c80)]:
|
|
53
|
+
- @vultisig/sdk@2.18.0
|
|
54
|
+
- @vultisig/rujira@51.0.0
|
|
55
|
+
|
|
3
56
|
## 2.17.0
|
|
4
57
|
|
|
5
58
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -5316,7 +5316,9 @@ var EXIT_CODE_DESCRIPTIONS = {
|
|
|
5316
5316
|
[6 /* EXTERNAL_SERVICE */]: "External service error (retryable)",
|
|
5317
5317
|
[7 /* UNKNOWN */]: "Unknown/unexpected error",
|
|
5318
5318
|
[8 /* ACK_FAILED */]: "Broadcast succeeded but post-broadcast report failed \u2014 hash is valid, do NOT retry",
|
|
5319
|
-
[9 /* DUPLICATE_BROADCAST */]: "Duplicate broadcast refused (nothing sent) \u2014 retry with --force to override"
|
|
5319
|
+
[9 /* DUPLICATE_BROADCAST */]: "Duplicate broadcast refused (nothing sent) \u2014 retry with --force to override",
|
|
5320
|
+
[10 /* AGENT_TURN_BLOCKED */]: "agent ask: a fund-safety guardrail blocked the requested action",
|
|
5321
|
+
[11 /* AGENT_TURN_REFUSAL */]: "agent ask: the model refused or asked a clarifying question (no action taken)"
|
|
5320
5322
|
};
|
|
5321
5323
|
var VsigError = class extends Error {
|
|
5322
5324
|
hint;
|
|
@@ -5398,6 +5400,21 @@ var TokenNotFoundError = class extends VsigError {
|
|
|
5398
5400
|
super(message, hint, suggestions, context);
|
|
5399
5401
|
}
|
|
5400
5402
|
};
|
|
5403
|
+
var TxNotFoundError = class extends VsigError {
|
|
5404
|
+
exitCode = 5 /* RESOURCE_NOT_FOUND */;
|
|
5405
|
+
code = "TX_NOT_FOUND";
|
|
5406
|
+
constructor(message, hint, suggestions, context) {
|
|
5407
|
+
super(message, hint, suggestions, context);
|
|
5408
|
+
}
|
|
5409
|
+
};
|
|
5410
|
+
var TxStatusTimeoutError = class extends VsigError {
|
|
5411
|
+
exitCode = 3 /* NETWORK */;
|
|
5412
|
+
code = "TX_STATUS_TIMEOUT";
|
|
5413
|
+
retryable = true;
|
|
5414
|
+
constructor(message, hint, suggestions, context) {
|
|
5415
|
+
super(message, hint, suggestions, context);
|
|
5416
|
+
}
|
|
5417
|
+
};
|
|
5401
5418
|
var ExternalServiceError = class extends VsigError {
|
|
5402
5419
|
exitCode = 6 /* EXTERNAL_SERVICE */;
|
|
5403
5420
|
code = "EXTERNAL_SERVICE";
|
|
@@ -6536,33 +6553,78 @@ async function executeBroadcast(ctx2, params) {
|
|
|
6536
6553
|
}
|
|
6537
6554
|
|
|
6538
6555
|
// src/commands/tx-status.ts
|
|
6539
|
-
import { Chain as Chain5, Vultisig as Vultisig5 } from "@vultisig/sdk";
|
|
6556
|
+
import { Chain as Chain5, isValidTxHash, Vultisig as Vultisig5 } from "@vultisig/sdk";
|
|
6540
6557
|
var POLL_INTERVAL_MS = 5e3;
|
|
6541
|
-
|
|
6558
|
+
var DEFAULT_TIMEOUT_SEC = 120;
|
|
6559
|
+
var isTerminal = (status) => status === "success" || status === "error";
|
|
6560
|
+
function resolveTimeoutMs(timeoutSec) {
|
|
6561
|
+
if (typeof timeoutSec !== "number" || !Number.isFinite(timeoutSec)) {
|
|
6562
|
+
return DEFAULT_TIMEOUT_SEC * 1e3;
|
|
6563
|
+
}
|
|
6564
|
+
return Math.max(0, timeoutSec) * 1e3;
|
|
6565
|
+
}
|
|
6566
|
+
async function executeTxStatus(ctx2, params, opts = {}) {
|
|
6542
6567
|
const vault = await ctx2.ensureActiveVault();
|
|
6543
6568
|
if (!Object.values(Chain5).includes(params.chain)) {
|
|
6544
|
-
throw new
|
|
6569
|
+
throw new InvalidInputError(`Invalid chain: ${params.chain}`);
|
|
6570
|
+
}
|
|
6571
|
+
if (!isValidTxHash(params.chain, params.txHash)) {
|
|
6572
|
+
throw new InvalidInputError(
|
|
6573
|
+
`Invalid transaction hash for ${params.chain}: "${params.txHash}"`,
|
|
6574
|
+
"Check the hash \u2014 it must match the expected format for the chain.",
|
|
6575
|
+
void 0,
|
|
6576
|
+
{ chain: params.chain, txHash: params.txHash }
|
|
6577
|
+
);
|
|
6545
6578
|
}
|
|
6579
|
+
const pollIntervalMs = opts.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
6546
6580
|
const spinner = createSpinner("Checking transaction status...");
|
|
6547
6581
|
try {
|
|
6548
6582
|
let result = await vault.getTxStatus({ chain: params.chain, txHash: params.txHash });
|
|
6549
|
-
if (!params.noWait) {
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6583
|
+
if (!params.noWait && !isTerminal(result.status)) {
|
|
6584
|
+
const deadline = Date.now() + resolveTimeoutMs(params.timeoutSec);
|
|
6585
|
+
let waited = 0;
|
|
6586
|
+
while (!isTerminal(result.status)) {
|
|
6587
|
+
const remainingMs = deadline - Date.now();
|
|
6588
|
+
if (remainingMs <= 0) {
|
|
6589
|
+
spinner.fail(`Gave up waiting after ${Math.round(waited / 1e3)}s (status: ${result.status})`);
|
|
6590
|
+
throw giveUpError(params, result, waited);
|
|
6591
|
+
}
|
|
6592
|
+
const sleepMs = Math.min(pollIntervalMs, remainingMs);
|
|
6593
|
+
waited += sleepMs;
|
|
6594
|
+
spinner.text = `Transaction ${result.status}... (${Math.round(waited / 1e3)}s)`;
|
|
6595
|
+
await sleep(sleepMs);
|
|
6554
6596
|
result = await vault.getTxStatus({ chain: params.chain, txHash: params.txHash });
|
|
6555
|
-
polls++;
|
|
6556
6597
|
}
|
|
6557
6598
|
}
|
|
6558
6599
|
spinner.succeed(`Transaction status: ${result.status}`);
|
|
6559
6600
|
displayResult(params.chain, params.txHash, result);
|
|
6560
6601
|
return result;
|
|
6561
6602
|
} catch (error2) {
|
|
6603
|
+
if (error2 instanceof TxNotFoundError || error2 instanceof TxStatusTimeoutError) {
|
|
6604
|
+
throw error2;
|
|
6605
|
+
}
|
|
6562
6606
|
spinner.fail("Failed to check transaction status");
|
|
6563
6607
|
throw error2;
|
|
6564
6608
|
}
|
|
6565
6609
|
}
|
|
6610
|
+
function giveUpError(params, result, waitedMs) {
|
|
6611
|
+
const seconds = Math.round(waitedMs / 1e3);
|
|
6612
|
+
const context = { chain: params.chain, txHash: params.txHash, status: result.status };
|
|
6613
|
+
if (result.status === "not_found") {
|
|
6614
|
+
return new TxNotFoundError(
|
|
6615
|
+
`Transaction not found on ${params.chain} after ${seconds}s: ${params.txHash}`,
|
|
6616
|
+
"The node has no record of this hash \u2014 it may have been dropped, replaced, or never broadcast.",
|
|
6617
|
+
["Verify the transaction hash", "Re-broadcast if it was never sent"],
|
|
6618
|
+
context
|
|
6619
|
+
);
|
|
6620
|
+
}
|
|
6621
|
+
return new TxStatusTimeoutError(
|
|
6622
|
+
`Transaction still ${result.status} on ${params.chain} after ${seconds}s: ${params.txHash}`,
|
|
6623
|
+
"The transaction may still confirm later.",
|
|
6624
|
+
["Re-run to keep checking", "Increase the wait budget with --timeout <seconds>"],
|
|
6625
|
+
context
|
|
6626
|
+
);
|
|
6627
|
+
}
|
|
6566
6628
|
function displayResult(chain, txHash, result) {
|
|
6567
6629
|
if (isJsonOutput()) {
|
|
6568
6630
|
outputJson({
|
|
@@ -8249,6 +8311,7 @@ var AskInterface = class {
|
|
|
8249
8311
|
toolCalls = [];
|
|
8250
8312
|
transactions = [];
|
|
8251
8313
|
cards = [];
|
|
8314
|
+
outcome;
|
|
8252
8315
|
error;
|
|
8253
8316
|
// Initialize-time error, kept SEPARATE from the turn error so it stays the
|
|
8254
8317
|
// LOWEST-priority signal. initialize() drives getCallbacks() BEFORE the first
|
|
@@ -8311,6 +8374,9 @@ var AskInterface = class {
|
|
|
8311
8374
|
onBalanceSummary: (card) => {
|
|
8312
8375
|
this.cards.push(card);
|
|
8313
8376
|
},
|
|
8377
|
+
onTurnOutcome: (outcome) => {
|
|
8378
|
+
this.outcome = outcome;
|
|
8379
|
+
},
|
|
8314
8380
|
onSuggestions: (_suggestions) => {
|
|
8315
8381
|
},
|
|
8316
8382
|
onTxStatus: (txHash, chain, status, explorerUrl) => {
|
|
@@ -8327,12 +8393,12 @@ var AskInterface = class {
|
|
|
8327
8393
|
}
|
|
8328
8394
|
},
|
|
8329
8395
|
onError: (message, code) => {
|
|
8330
|
-
const
|
|
8396
|
+
const isTerminal2 = isTerminalAgentErrorCode(code);
|
|
8331
8397
|
if (!this.hasAsked) {
|
|
8332
8398
|
this.initError = { message, code };
|
|
8333
|
-
} else if (!this.error ||
|
|
8399
|
+
} else if (!this.error || isTerminal2 && !this.errorIsTerminal) {
|
|
8334
8400
|
this.error = { message, code };
|
|
8335
|
-
this.errorIsTerminal =
|
|
8401
|
+
this.errorIsTerminal = isTerminal2;
|
|
8336
8402
|
}
|
|
8337
8403
|
process.stderr.write(`[error] ${message} [${code}]
|
|
8338
8404
|
`);
|
|
@@ -8363,6 +8429,7 @@ var AskInterface = class {
|
|
|
8363
8429
|
this.toolCalls = [];
|
|
8364
8430
|
this.transactions = [];
|
|
8365
8431
|
this.cards = [];
|
|
8432
|
+
this.outcome = void 0;
|
|
8366
8433
|
this.error = void 0;
|
|
8367
8434
|
if (this.hasAsked) {
|
|
8368
8435
|
this.initError = void 0;
|
|
@@ -8390,7 +8457,8 @@ var AskInterface = class {
|
|
|
8390
8457
|
cards: this.cards,
|
|
8391
8458
|
// A real turn error wins; fall back to the init-time signal (e.g. stale
|
|
8392
8459
|
// --session SESSION_NOT_FOUND) only when the turn produced no error.
|
|
8393
|
-
error: this.error ?? this.initError
|
|
8460
|
+
error: this.error ?? this.initError,
|
|
8461
|
+
...this.outcome ? { outcome: this.outcome } : {}
|
|
8394
8462
|
};
|
|
8395
8463
|
}
|
|
8396
8464
|
};
|
|
@@ -8797,7 +8865,19 @@ function padTo32Bytes(buf) {
|
|
|
8797
8865
|
|
|
8798
8866
|
// src/agent/cards.ts
|
|
8799
8867
|
import chalk8 from "chalk";
|
|
8800
|
-
var CLI_SUPPORTED_SURFACES = ["balance_summary"];
|
|
8868
|
+
var CLI_SUPPORTED_SURFACES = ["balance_summary", "turn_outcome"];
|
|
8869
|
+
function parseTurnOutcome(raw) {
|
|
8870
|
+
if (!raw || typeof raw !== "object") return null;
|
|
8871
|
+
const kind = raw.kind;
|
|
8872
|
+
if (kind !== "success" && kind !== "blocked" && kind !== "refusal" && kind !== "error") return null;
|
|
8873
|
+
const code = raw.code;
|
|
8874
|
+
const detail = raw.detail;
|
|
8875
|
+
return {
|
|
8876
|
+
kind,
|
|
8877
|
+
...typeof code === "string" ? { code } : {},
|
|
8878
|
+
...typeof detail === "string" ? { detail } : {}
|
|
8879
|
+
};
|
|
8880
|
+
}
|
|
8801
8881
|
function stripControlChars(s) {
|
|
8802
8882
|
let out = "";
|
|
8803
8883
|
for (const ch of s) {
|
|
@@ -11749,7 +11829,7 @@ var CLI_SIGNABLE_FLAT_TOOLS = /* @__PURE__ */ new Set([
|
|
|
11749
11829
|
"build_max_subscription_renewal",
|
|
11750
11830
|
"build_pro_subscription_renewal"
|
|
11751
11831
|
]);
|
|
11752
|
-
var
|
|
11832
|
+
var CLI_SIGNABLE_PREP_TOOLS = /* @__PURE__ */ new Set([
|
|
11753
11833
|
"execute_swap",
|
|
11754
11834
|
"execute_send",
|
|
11755
11835
|
"execute_contract_call"
|
|
@@ -11809,6 +11889,22 @@ function asChainIdString(value) {
|
|
|
11809
11889
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
11810
11890
|
return void 0;
|
|
11811
11891
|
}
|
|
11892
|
+
function resolvePrepChain(txArgs) {
|
|
11893
|
+
const byName = asChainString(txArgs.chain) ? resolveChain(asChainString(txArgs.chain)) : null;
|
|
11894
|
+
const byId = asChainIdString(txArgs.chain_id) ? resolveChainId(asChainIdString(txArgs.chain_id)) : null;
|
|
11895
|
+
if (!byName && !byId) return null;
|
|
11896
|
+
if (byName && byId && byName !== byId) return null;
|
|
11897
|
+
return byName ?? byId;
|
|
11898
|
+
}
|
|
11899
|
+
function resolvePrepParentChain(env) {
|
|
11900
|
+
const byChain = asChainString(env.chain) ? resolveChain(asChainString(env.chain)) : null;
|
|
11901
|
+
const byFromChain = asChainString(env.from_chain) ? resolveChain(asChainString(env.from_chain)) : null;
|
|
11902
|
+
const byId = asChainIdString(env.chain_id) ? resolveChainId(asChainIdString(env.chain_id)) : null;
|
|
11903
|
+
const candidates = [byChain, byFromChain, byId].filter((c) => c !== null);
|
|
11904
|
+
if (candidates.length === 0) return null;
|
|
11905
|
+
if (candidates.some((c) => c !== candidates[0])) return null;
|
|
11906
|
+
return candidates[0];
|
|
11907
|
+
}
|
|
11812
11908
|
function buildTxReadyFromToolOutput(toolName, output) {
|
|
11813
11909
|
if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName)) return null;
|
|
11814
11910
|
const env = asRecord(output);
|
|
@@ -11843,12 +11939,24 @@ function deriveToolOutputCandidate(toolName, output) {
|
|
|
11843
11939
|
const payload = buildTxReadyFromToolOutput(toolName, output);
|
|
11844
11940
|
return payload ? { payload, source: "flat", toolName } : null;
|
|
11845
11941
|
}
|
|
11846
|
-
if (
|
|
11942
|
+
if (CLI_SIGNABLE_PREP_TOOLS.has(toolName)) {
|
|
11847
11943
|
const env = asRecord(output);
|
|
11848
11944
|
if (!env || env.status === "error" || "error" in env) return null;
|
|
11849
11945
|
const txArgs = asRecord(env.txArgs);
|
|
11850
11946
|
if (!txArgs) return null;
|
|
11851
11947
|
if (typeof txArgs.tx_encoding !== "string" || txArgs.tx_encoding === "") return null;
|
|
11948
|
+
const prepChain = resolvePrepChain(txArgs);
|
|
11949
|
+
if (!prepChain) return null;
|
|
11950
|
+
const hasParentChainMetadata = asChainString(env.chain) !== void 0 || asChainString(env.from_chain) !== void 0 || asChainIdString(env.chain_id) !== void 0;
|
|
11951
|
+
if (hasParentChainMetadata) {
|
|
11952
|
+
const parentChain = resolvePrepParentChain(env);
|
|
11953
|
+
if (!parentChain || parentChain !== prepChain) return null;
|
|
11954
|
+
}
|
|
11955
|
+
const approvalTxArgs = asRecord(env.approvalTxArgs);
|
|
11956
|
+
if (approvalTxArgs) {
|
|
11957
|
+
const approvalChain = resolvePrepChain(approvalTxArgs);
|
|
11958
|
+
if (!approvalChain || approvalChain !== prepChain) return null;
|
|
11959
|
+
}
|
|
11852
11960
|
return { payload: env, source: "prep", toolName };
|
|
11853
11961
|
}
|
|
11854
11962
|
return null;
|
|
@@ -11858,105 +11966,6 @@ function str(value) {
|
|
|
11858
11966
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
11859
11967
|
return void 0;
|
|
11860
11968
|
}
|
|
11861
|
-
function nestedSource(legObj) {
|
|
11862
|
-
const nested = (asRecord(legObj.tx) || asRecord(legObj.swap_tx) || asRecord(legObj.send_tx) || asRecord(legObj.txArgs?.tx)) ?? asRecord(legObj.txArgs) ?? legObj;
|
|
11863
|
-
return nested;
|
|
11864
|
-
}
|
|
11865
|
-
function canonGasLimit(value) {
|
|
11866
|
-
if (typeof value === "number" && Number.isInteger(value) && value > 0) return String(value);
|
|
11867
|
-
if (typeof value === "string") {
|
|
11868
|
-
const t = value.trim();
|
|
11869
|
-
if (/^\d+$/.test(t) || /^0x[0-9a-fA-F]+$/.test(t)) {
|
|
11870
|
-
try {
|
|
11871
|
-
const n = BigInt(t);
|
|
11872
|
-
return n > 0n ? n.toString() : void 0;
|
|
11873
|
-
} catch {
|
|
11874
|
-
return void 0;
|
|
11875
|
-
}
|
|
11876
|
-
}
|
|
11877
|
-
}
|
|
11878
|
-
return void 0;
|
|
11879
|
-
}
|
|
11880
|
-
function canonLeg(legObj) {
|
|
11881
|
-
const src = nestedSource(legObj);
|
|
11882
|
-
const txArgs = asRecord(legObj.txArgs);
|
|
11883
|
-
const toRaw = str(src.to) ?? str(src.to_address);
|
|
11884
|
-
const dataRaw = str(src.data) ?? str(src.calldata);
|
|
11885
|
-
const leg = {
|
|
11886
|
-
to: toRaw ? toRaw.toLowerCase() : void 0,
|
|
11887
|
-
value: normalizeValue(src.value),
|
|
11888
|
-
data: dataRaw ? dataRaw.toLowerCase() : void 0,
|
|
11889
|
-
gasLimit: canonGasLimit(src.gas_limit),
|
|
11890
|
-
chain: str(legObj.chain) ?? str(txArgs?.chain) ?? str(src.chain),
|
|
11891
|
-
chainId: str(legObj.chain_id) ?? str(txArgs?.chain_id) ?? str(src.chain_id),
|
|
11892
|
-
txEncoding: str(src.tx_encoding) ?? str(txArgs?.tx_encoding),
|
|
11893
|
-
amount: str(src.amount) ?? str(txArgs?.amount),
|
|
11894
|
-
memo: str(src.memo) ?? str(txArgs?.memo)
|
|
11895
|
-
};
|
|
11896
|
-
return leg;
|
|
11897
|
-
}
|
|
11898
|
-
var TX_READY_EXCLUSIVE_KEYS = ["typed_confirm", "sequence_id", "sequence_index", "sequence_total"];
|
|
11899
|
-
function collectExclusive(payload) {
|
|
11900
|
-
const found = [];
|
|
11901
|
-
const scan = (obj) => {
|
|
11902
|
-
if (!obj) return;
|
|
11903
|
-
for (const k of TX_READY_EXCLUSIVE_KEYS) if (k in obj && !found.includes(k)) found.push(k);
|
|
11904
|
-
};
|
|
11905
|
-
scan(payload);
|
|
11906
|
-
scan(asRecord(payload.tx));
|
|
11907
|
-
scan(asRecord(payload.txArgs));
|
|
11908
|
-
return found;
|
|
11909
|
-
}
|
|
11910
|
-
function canonicalizeForParity(payload) {
|
|
11911
|
-
const env = asRecord(payload);
|
|
11912
|
-
if (!env) return null;
|
|
11913
|
-
const approval = asRecord(env.approvalTxArgs);
|
|
11914
|
-
const main = asRecord(env.txArgs);
|
|
11915
|
-
const legs = approval && main ? [canonLeg(approval), canonLeg(main)] : [canonLeg(env)];
|
|
11916
|
-
return { legs, exclusive: collectExclusive(env) };
|
|
11917
|
-
}
|
|
11918
|
-
var HARD_FIELDS = [
|
|
11919
|
-
"to",
|
|
11920
|
-
"value",
|
|
11921
|
-
"data",
|
|
11922
|
-
"chain",
|
|
11923
|
-
"chainId",
|
|
11924
|
-
"txEncoding",
|
|
11925
|
-
"amount",
|
|
11926
|
-
"memo",
|
|
11927
|
-
// gas_limit is signing-relevant, NOT advisory: signEvmServerTx copies a
|
|
11928
|
-
// server-supplied gas_limit into the signed ethereumSpecific.gasLimit when it
|
|
11929
|
-
// exceeds the SDK estimate, so a gas_limit divergence means different signed
|
|
11930
|
-
// bytes/fee. Parity must surface it.
|
|
11931
|
-
"gasLimit"
|
|
11932
|
-
];
|
|
11933
|
-
function diffToolOutputParity(enriched, txReady) {
|
|
11934
|
-
const ce = canonicalizeForParity(enriched);
|
|
11935
|
-
const ct = canonicalizeForParity(txReady);
|
|
11936
|
-
const divergences = [];
|
|
11937
|
-
if (!ce || !ct) {
|
|
11938
|
-
return {
|
|
11939
|
-
match: false,
|
|
11940
|
-
divergences: [`uncomparable payload (enriched=${ce ? "ok" : "null"}, tx_ready=${ct ? "ok" : "null"})`],
|
|
11941
|
-
txReadyExclusive: ct?.exclusive ?? []
|
|
11942
|
-
};
|
|
11943
|
-
}
|
|
11944
|
-
if (ce.legs.length !== ct.legs.length) {
|
|
11945
|
-
divergences.push(`leg count: tool-output ${ce.legs.length} vs tx_ready ${ct.legs.length}`);
|
|
11946
|
-
}
|
|
11947
|
-
const n = Math.min(ce.legs.length, ct.legs.length);
|
|
11948
|
-
for (let i = 0; i < n; i++) {
|
|
11949
|
-
for (const f of HARD_FIELDS) {
|
|
11950
|
-
const a = ce.legs[i][f];
|
|
11951
|
-
const b = ct.legs[i][f];
|
|
11952
|
-
if (a !== b && !(a === void 0 && b === void 0)) {
|
|
11953
|
-
divergences.push(`leg[${i}].${f}: tool-output ${a ?? "\u2205"} vs tx_ready ${b ?? "\u2205"}`);
|
|
11954
|
-
}
|
|
11955
|
-
}
|
|
11956
|
-
}
|
|
11957
|
-
const txReadyExclusive = ct.exclusive.filter((k) => !ce.exclusive.includes(k));
|
|
11958
|
-
return { match: divergences.length === 0, divergences, txReadyExclusive };
|
|
11959
|
-
}
|
|
11960
11969
|
function payloadLooksSignable(payload) {
|
|
11961
11970
|
const env = asRecord(payload);
|
|
11962
11971
|
if (!env) return false;
|
|
@@ -12246,7 +12255,6 @@ var AgentClient = class {
|
|
|
12246
12255
|
const result = {
|
|
12247
12256
|
fullText: "",
|
|
12248
12257
|
suggestions: [],
|
|
12249
|
-
transactions: [],
|
|
12250
12258
|
message: null,
|
|
12251
12259
|
finished: false,
|
|
12252
12260
|
disconnected: false,
|
|
@@ -12257,7 +12265,6 @@ var AgentClient = class {
|
|
|
12257
12265
|
serverNow: res.headers.get("X-Server-Now")
|
|
12258
12266
|
};
|
|
12259
12267
|
const toolNameByCallId = /* @__PURE__ */ new Map();
|
|
12260
|
-
const parity = { lastSignableToolCallId: null };
|
|
12261
12268
|
const reader = res.body.getReader();
|
|
12262
12269
|
const decoder = new TextDecoder();
|
|
12263
12270
|
let buffer = "";
|
|
@@ -12272,7 +12279,7 @@ var AgentClient = class {
|
|
|
12272
12279
|
currentData += (currentData ? "\n" : "") + stripLeadingSpace(line.slice(5));
|
|
12273
12280
|
} else if (line === "") {
|
|
12274
12281
|
if (currentData) {
|
|
12275
|
-
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId
|
|
12282
|
+
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId);
|
|
12276
12283
|
}
|
|
12277
12284
|
currentEvent = "";
|
|
12278
12285
|
currentData = "";
|
|
@@ -12292,7 +12299,7 @@ var AgentClient = class {
|
|
|
12292
12299
|
if (done) {
|
|
12293
12300
|
if (trailing) processLine(trailing);
|
|
12294
12301
|
if (currentData) {
|
|
12295
|
-
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId
|
|
12302
|
+
this.handleSSEEvent(currentEvent || "message", currentData, result, callbacks, toolNameByCallId);
|
|
12296
12303
|
}
|
|
12297
12304
|
break;
|
|
12298
12305
|
}
|
|
@@ -12309,7 +12316,7 @@ var AgentClient = class {
|
|
|
12309
12316
|
}
|
|
12310
12317
|
return result;
|
|
12311
12318
|
}
|
|
12312
|
-
handleSSEEvent(event, data, result, callbacks, toolNameByCallId
|
|
12319
|
+
handleSSEEvent(event, data, result, callbacks, toolNameByCallId) {
|
|
12313
12320
|
try {
|
|
12314
12321
|
const parsed = JSON.parse(data);
|
|
12315
12322
|
const v1Type = getV1Type(parsed);
|
|
@@ -12320,7 +12327,7 @@ var AgentClient = class {
|
|
|
12320
12327
|
this.handleTextDelta(parsed, result, callbacks);
|
|
12321
12328
|
break;
|
|
12322
12329
|
case "tool_progress":
|
|
12323
|
-
this.handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type
|
|
12330
|
+
this.handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type);
|
|
12324
12331
|
break;
|
|
12325
12332
|
case "title": {
|
|
12326
12333
|
const title = v1Data?.title ?? parsed.title;
|
|
@@ -12333,20 +12340,16 @@ var AgentClient = class {
|
|
|
12333
12340
|
callbacks.onSuggestions?.(suggestions);
|
|
12334
12341
|
break;
|
|
12335
12342
|
}
|
|
12336
|
-
case "tx_ready":
|
|
12337
|
-
if (this.verbose) process.stderr.write(`[SSE:tx_ready] raw: ${data.slice(0, 2e3)}
|
|
12338
|
-
`);
|
|
12339
|
-
{
|
|
12340
|
-
const txReady = v1Data ?? parsed;
|
|
12341
|
-
result.transactions.push(txReady);
|
|
12342
|
-
callbacks.onTxReady?.(txReady, parity.lastSignableToolCallId ?? void 0);
|
|
12343
|
-
}
|
|
12344
|
-
break;
|
|
12345
12343
|
case "balance_summary": {
|
|
12346
12344
|
const card = v1Data ?? parsed.data ?? parsed;
|
|
12347
12345
|
callbacks.onBalanceSummary?.(card);
|
|
12348
12346
|
break;
|
|
12349
12347
|
}
|
|
12348
|
+
case "turn_outcome": {
|
|
12349
|
+
const outcome = parseTurnOutcome(v1Data ?? parsed.data ?? parsed);
|
|
12350
|
+
if (outcome) callbacks.onTurnOutcome?.(outcome);
|
|
12351
|
+
break;
|
|
12352
|
+
}
|
|
12350
12353
|
case "message": {
|
|
12351
12354
|
const msg = v1Data?.message ?? parsed.message ?? parsed;
|
|
12352
12355
|
result.message = msg;
|
|
@@ -12375,7 +12378,7 @@ var AgentClient = class {
|
|
|
12375
12378
|
result.fullText += parsed.delta;
|
|
12376
12379
|
callbacks.onTextDelta?.(parsed.delta);
|
|
12377
12380
|
}
|
|
12378
|
-
handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type
|
|
12381
|
+
handleToolProgress(parsed, data, callbacks, toolNameByCallId, v1Type) {
|
|
12379
12382
|
if (this.verbose) process.stderr.write(`[SSE:tool_progress] raw: ${data.slice(0, 1e3)}
|
|
12380
12383
|
`);
|
|
12381
12384
|
const status = parsed.status ?? v1StatusFromType(v1Type);
|
|
@@ -12388,32 +12391,27 @@ var AgentClient = class {
|
|
|
12388
12391
|
this.maybeEmitClientSideToolCall(parsed, callbacks, v1Type, callId, toolName);
|
|
12389
12392
|
const ok = deriveToolDoneOk(status, parsed.output);
|
|
12390
12393
|
if (status && toolName) callbacks.onToolProgress?.(toolName, status, label, ok);
|
|
12391
|
-
this.maybeSignToolOutput(status, toolName, parsed.output, callbacks
|
|
12394
|
+
this.maybeSignToolOutput(status, toolName, parsed.output, callbacks);
|
|
12392
12395
|
if (status === "done" && callId) toolNameByCallId.delete(callId);
|
|
12393
12396
|
}
|
|
12394
12397
|
/**
|
|
12395
|
-
* Phase
|
|
12396
|
-
*
|
|
12397
|
-
*
|
|
12398
|
-
* `tx_ready`
|
|
12399
|
-
*
|
|
12400
|
-
*
|
|
12401
|
-
*
|
|
12402
|
-
* `erc20_approve`, `build_custom_*`) → the PARITY reference; `tx_ready`
|
|
12403
|
-
* stays authoritative when signable.
|
|
12404
|
-
* The bridge guards against non-tx results (`no_op` / `insufficient_*` /
|
|
12405
|
-
* errors) so those never reach the signer. Zero backend change.
|
|
12398
|
+
* #927 Phase 2: derive a client-side signable candidate from a signable tool's
|
|
12399
|
+
* raw `tool-output-available` output — the same envelope mobile reads — and
|
|
12400
|
+
* hand it to the session via `onToolOutputTx` as the SOLE signing source
|
|
12401
|
+
* (production emits the payload here; `data-tx_ready` is a hollow marker). The
|
|
12402
|
+
* bridge guards against non-tx results (`no_op` / `insufficient_*` / errors)
|
|
12403
|
+
* and phantom-card prep envelopes so those never reach the signer. Zero backend
|
|
12404
|
+
* change.
|
|
12406
12405
|
*/
|
|
12407
|
-
maybeSignToolOutput(status, toolName, output, callbacks
|
|
12406
|
+
maybeSignToolOutput(status, toolName, output, callbacks) {
|
|
12408
12407
|
if (status !== "done" || !toolName || !callbacks.onToolOutputTx) return;
|
|
12409
|
-
if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName) && !
|
|
12408
|
+
if (!CLI_SIGNABLE_FLAT_TOOLS.has(toolName) && !CLI_SIGNABLE_PREP_TOOLS.has(toolName)) return;
|
|
12410
12409
|
const candidate = deriveToolOutputCandidate(toolName, output);
|
|
12411
12410
|
if (!candidate) return;
|
|
12412
|
-
if (callId) parity.lastSignableToolCallId = callId;
|
|
12413
12411
|
if (this.verbose)
|
|
12414
12412
|
process.stderr.write(`[SSE:tool_output] ${toolName} \u2192 onToolOutputTx (${candidate.source} candidate)
|
|
12415
12413
|
`);
|
|
12416
|
-
callbacks.onToolOutputTx(candidate.payload, toolName, candidate.source
|
|
12414
|
+
callbacks.onToolOutputTx(candidate.payload, toolName, candidate.source);
|
|
12417
12415
|
}
|
|
12418
12416
|
maybeEmitClientSideToolCall(parsed, callbacks, v1Type, callId, toolName) {
|
|
12419
12417
|
if (v1Type !== "tool-input-available" || !callId || !toolName || !this.clientSideToolNames.has(toolName) || !callbacks.onClientSideToolCall) {
|
|
@@ -12430,7 +12428,9 @@ var AgentClient = class {
|
|
|
12430
12428
|
// Maps a V1 `type` field to the legacy event bucket used by handleSSEEvent's
|
|
12431
12429
|
// switch. Frame-level types (start, text-start, text-end, finish-step) and
|
|
12432
12430
|
// non-critical telemetry (data-tokens, data-usage, data-confirmation) route
|
|
12433
|
-
// to 'ignore' which is a no-op.
|
|
12431
|
+
// to 'ignore' which is a no-op. `data-tx_ready` also routes to 'ignore':
|
|
12432
|
+
// #927 Phase 2 signs purely from `tool-output-available`, and production emits
|
|
12433
|
+
// `data-tx_ready` only as a hollow `{typed_confirm}` marker the CLI doesn't use.
|
|
12434
12434
|
mapV1EventType(type) {
|
|
12435
12435
|
switch (type) {
|
|
12436
12436
|
case "text-delta":
|
|
@@ -12444,10 +12444,10 @@ var AgentClient = class {
|
|
|
12444
12444
|
return "title";
|
|
12445
12445
|
case "data-suggestions":
|
|
12446
12446
|
return "suggestions";
|
|
12447
|
-
case "data-tx_ready":
|
|
12448
|
-
return "tx_ready";
|
|
12449
12447
|
case "data-balance_summary":
|
|
12450
12448
|
return "balance_summary";
|
|
12449
|
+
case "data-turn_outcome":
|
|
12450
|
+
return "turn_outcome";
|
|
12451
12451
|
case "data-message":
|
|
12452
12452
|
return "message";
|
|
12453
12453
|
case "error":
|
|
@@ -13239,8 +13239,6 @@ var AgentSession = class {
|
|
|
13239
13239
|
`);
|
|
13240
13240
|
}
|
|
13241
13241
|
}
|
|
13242
|
-
let txReadyCandidate = null;
|
|
13243
|
-
let txReadyTwinCallId = null;
|
|
13244
13242
|
let toolOutputCandidate = null;
|
|
13245
13243
|
let balanceCardRendered = false;
|
|
13246
13244
|
const pendingDispatches = [];
|
|
@@ -13266,20 +13264,12 @@ var AgentSession = class {
|
|
|
13266
13264
|
onSuggestions: (suggestions) => {
|
|
13267
13265
|
ui.onSuggestions(suggestions);
|
|
13268
13266
|
},
|
|
13269
|
-
|
|
13270
|
-
if (txReadyCandidate !== null) {
|
|
13271
|
-
this.reportDeferredSignable(ui);
|
|
13272
|
-
return;
|
|
13273
|
-
}
|
|
13274
|
-
txReadyCandidate = tx;
|
|
13275
|
-
txReadyTwinCallId = toolCallId ?? null;
|
|
13276
|
-
},
|
|
13277
|
-
onToolOutputTx: (payload, toolName, source, toolCallId) => {
|
|
13267
|
+
onToolOutputTx: (payload, toolName, source) => {
|
|
13278
13268
|
if (toolOutputCandidate !== null) {
|
|
13279
13269
|
this.reportDeferredSignable(ui);
|
|
13280
13270
|
return;
|
|
13281
13271
|
}
|
|
13282
|
-
toolOutputCandidate = { payload, toolName, source
|
|
13272
|
+
toolOutputCandidate = { payload, toolName, source };
|
|
13283
13273
|
},
|
|
13284
13274
|
onBalanceSummary: (raw) => {
|
|
13285
13275
|
const card = parseBalanceSummaryEnvelope(raw);
|
|
@@ -13288,6 +13278,9 @@ var AgentSession = class {
|
|
|
13288
13278
|
ui.onBalanceSummary?.(card);
|
|
13289
13279
|
}
|
|
13290
13280
|
},
|
|
13281
|
+
onTurnOutcome: (outcome) => {
|
|
13282
|
+
ui.onTurnOutcome?.(outcome);
|
|
13283
|
+
},
|
|
13291
13284
|
onMessage: (_msg) => {
|
|
13292
13285
|
},
|
|
13293
13286
|
onError: (error2, code) => {
|
|
@@ -13310,7 +13303,7 @@ var AgentSession = class {
|
|
|
13310
13303
|
}
|
|
13311
13304
|
if (streamResult.disconnected && !streamResult.message) {
|
|
13312
13305
|
ui.onReconnecting?.();
|
|
13313
|
-
await this.recoverDisconnectedTurn(streamResult, callbacks.
|
|
13306
|
+
await this.recoverDisconnectedTurn(streamResult, callbacks.onBalanceSummary);
|
|
13314
13307
|
}
|
|
13315
13308
|
const responseText = streamResult.message?.content || streamResult.fullText || "";
|
|
13316
13309
|
let displayText = stripLeakedToolCallTags(responseText);
|
|
@@ -13320,7 +13313,7 @@ var AgentSession = class {
|
|
|
13320
13313
|
if (displayText) {
|
|
13321
13314
|
ui.onAssistantMessage(displayText);
|
|
13322
13315
|
}
|
|
13323
|
-
const bufferedSignable = this.selectAndBufferSignable(
|
|
13316
|
+
const bufferedSignable = this.selectAndBufferSignable(toolOutputCandidate);
|
|
13324
13317
|
if (bufferedSignable) {
|
|
13325
13318
|
if (this.config.verbose) process.stderr.write(`[session] buffered a signable server tx, signing client-side
|
|
13326
13319
|
`);
|
|
@@ -13370,102 +13363,42 @@ var AgentSession = class {
|
|
|
13370
13363
|
);
|
|
13371
13364
|
}
|
|
13372
13365
|
/**
|
|
13373
|
-
* Phase
|
|
13374
|
-
*
|
|
13375
|
-
*
|
|
13376
|
-
*
|
|
13377
|
-
* Selection order (fund-safety):
|
|
13378
|
-
* 1. a SIGNABLE `tx_ready` — AUTHORITATIVE, unchanged from today's behavior;
|
|
13379
|
-
* 2. the client tool-output candidate — covers flat off-chain tools with no
|
|
13380
|
-
* `tx_ready` (polymarket) and flat tools whose `tx_ready` is structurally
|
|
13381
|
-
* unsignable (`build_custom_*`, divergent `to_address`/`calldata`);
|
|
13382
|
-
* 3. an unsignable `tx_ready` as last resort — preserves today's behavior (it
|
|
13383
|
-
* surfaces its own error at sign time) rather than silently dropping it.
|
|
13384
|
-
* `payloadLooksSignable` mirrors the executor's real requirements so a
|
|
13385
|
-
* structurally-present-but-unsignable `tx_ready` (which `storeServerTransaction`
|
|
13386
|
-
* would still buffer, only to throw at sign time) doesn't pre-empt a good
|
|
13387
|
-
* tool-output candidate.
|
|
13388
|
-
*
|
|
13389
|
-
* FAIL-CLOSED on divergence (review — gomesalexandre): a flat tool-output is a
|
|
13390
|
-
* sign source ONLY when (a) there is NO `tx_ready` at all, or (b) the same-turn
|
|
13391
|
-
* `tx_ready` MATCHED parity. Whenever a `tx_ready` is present and the flat did
|
|
13392
|
-
* not prove equal to it, we do NOT sign the client-enriched candidate — we fall
|
|
13393
|
-
* closed to the `tx_ready` path (which, when unsignable, errors at sign time).
|
|
13394
|
-
*
|
|
13395
|
-
* This sign gate is DELIBERATELY INDEPENDENT of the pairing heuristic below:
|
|
13396
|
-
* whether the two same-turn candidates are "the same tool call" affects only
|
|
13397
|
-
* divergence TELEMETRY, never the sign decision. So even if the pairing were
|
|
13398
|
-
* wrong (e.g. the backend batched two tool-output frames before a `tx_ready`
|
|
13399
|
-
* against the documented per-pending order — agent-backend agent.go:6397), a
|
|
13400
|
-
* diverging flat still cannot be signed: it is gated by `parityMatched`, not by
|
|
13401
|
-
* pairing. The fund-safety guarantee therefore rests on parity equality alone,
|
|
13402
|
-
* not on a cross-repo wire-ordering invariant (security + Codex convergence).
|
|
13366
|
+
* #927 Phase 2 sign gate. Buffer the client-enriched tool-output candidate —
|
|
13367
|
+
* the SOLE signing source — into the executor. Returns true when something was
|
|
13368
|
+
* buffered (signed downstream), false otherwise.
|
|
13403
13369
|
*
|
|
13404
|
-
*
|
|
13405
|
-
*
|
|
13406
|
-
*
|
|
13407
|
-
*
|
|
13408
|
-
*
|
|
13409
|
-
*
|
|
13410
|
-
*
|
|
13370
|
+
* FAIL-CLOSED, two structural gates:
|
|
13371
|
+
* 1. `deriveToolOutputCandidate` (client.ts) already rejected non-tx envelopes
|
|
13372
|
+
* (`no_op` / `insufficient_*` / errors) and phantom-card prep envelopes
|
|
13373
|
+
* (no `tx_encoding`) — so a candidate that reaches here is well-formed.
|
|
13374
|
+
* 2. `payloadLooksSignable` mirrors the executor's real signer requirements
|
|
13375
|
+
* (`extractNestedTx().to` for EVM; `txArgs.{to,amount}` for non-EVM; both
|
|
13376
|
+
* legs for multi-leg). A structurally-present-but-unsignable candidate is
|
|
13377
|
+
* NOT routed to the signer — the mutation-check target for this gate.
|
|
13378
|
+
* `storeServerTransaction` is the final backstop (returns false if it can't
|
|
13379
|
+
* resolve a tx body / cross-chain multi-leg), so a false is never a wrong sign.
|
|
13411
13380
|
*/
|
|
13412
|
-
selectAndBufferSignable(
|
|
13413
|
-
|
|
13414
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
13417
|
-
|
|
13418
|
-
|
|
13419
|
-
|
|
13420
|
-
if (txReadyCandidate && payloadLooksSignable(txReadyCandidate)) {
|
|
13421
|
-
ordered.push({ surface: "tx_ready", payload: txReadyCandidate });
|
|
13422
|
-
}
|
|
13423
|
-
const flatIsSignSource = toolOutputCandidate?.source === "flat" && (txReadyCandidate === null || parityMatched);
|
|
13424
|
-
if (toolOutputCandidate && flatIsSignSource) {
|
|
13425
|
-
ordered.push({ surface: "tool_output", payload: toolOutputCandidate.payload });
|
|
13426
|
-
}
|
|
13427
|
-
if (txReadyCandidate && !payloadLooksSignable(txReadyCandidate)) {
|
|
13428
|
-
ordered.push({ surface: "tx_ready", payload: txReadyCandidate });
|
|
13429
|
-
}
|
|
13430
|
-
for (const { surface, payload } of ordered) {
|
|
13431
|
-
if (this.executor.storeServerTransaction(payload)) {
|
|
13432
|
-
if (this.config.password) this.executor.setPassword(this.config.password);
|
|
13433
|
-
if (this.config.verbose) {
|
|
13434
|
-
const both = txReadyCandidate && toolOutputCandidate ? "both channels" : "single channel";
|
|
13435
|
-
process.stderr.write(`[session] buffered signable from ${surface} (${both})
|
|
13436
|
-
`);
|
|
13437
|
-
}
|
|
13438
|
-
if (surface === "tx_ready" && toolOutputCandidate?.source === "flat" && !parityMatched) {
|
|
13439
|
-
this.reportDeferredSignable(ui);
|
|
13440
|
-
}
|
|
13441
|
-
return true;
|
|
13381
|
+
selectAndBufferSignable(toolOutputCandidate) {
|
|
13382
|
+
if (!toolOutputCandidate) return false;
|
|
13383
|
+
if (!payloadLooksSignable(toolOutputCandidate.payload)) {
|
|
13384
|
+
if (this.config.verbose) {
|
|
13385
|
+
process.stderr.write(
|
|
13386
|
+
`[session] tool-output candidate (${toolOutputCandidate.toolName}, ${toolOutputCandidate.source}) not structurally signable; not buffering
|
|
13387
|
+
`
|
|
13388
|
+
);
|
|
13442
13389
|
}
|
|
13390
|
+
return false;
|
|
13443
13391
|
}
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
/**
|
|
13447
|
-
* Compare the client-enriched tool-output candidate to the backend `tx_ready`
|
|
13448
|
-
* for the same turn and log the result. On a safety-relevant divergence this
|
|
13449
|
-
* logs LOUDLY (always to stderr, not just under --verbose) — that divergence
|
|
13450
|
-
* signal is exactly what Phase 1 exists to surface before Phase 2 makes
|
|
13451
|
-
* tool-output the sole signing source.
|
|
13452
|
-
*/
|
|
13453
|
-
logToolOutputParity(toolOutput, txReady) {
|
|
13454
|
-
const result = diffToolOutputParity(toolOutput.payload, txReady);
|
|
13455
|
-
const exclusive = result.txReadyExclusive.length ? ` | tx_ready-exclusive: ${result.txReadyExclusive.join(",")}` : "";
|
|
13456
|
-
if (result.match) {
|
|
13392
|
+
if (this.executor.storeServerTransaction(toolOutputCandidate.payload)) {
|
|
13393
|
+
if (this.config.password) this.executor.setPassword(this.config.password);
|
|
13457
13394
|
if (this.config.verbose) {
|
|
13458
13395
|
process.stderr.write(
|
|
13459
|
-
`[
|
|
13396
|
+
`[session] buffered signable from tool-output (${toolOutputCandidate.toolName}, ${toolOutputCandidate.source})
|
|
13460
13397
|
`
|
|
13461
13398
|
);
|
|
13462
13399
|
}
|
|
13463
13400
|
return true;
|
|
13464
13401
|
}
|
|
13465
|
-
process.stderr.write(
|
|
13466
|
-
`[parity][DIVERGENCE] ${toolOutput.toolName} (${toolOutput.source}): tool-output != tx_ready \u2014 ${result.divergences.join("; ")}${exclusive}
|
|
13467
|
-
`
|
|
13468
|
-
);
|
|
13469
13402
|
return false;
|
|
13470
13403
|
}
|
|
13471
13404
|
/**
|
|
@@ -13473,15 +13406,14 @@ var AgentSession = class {
|
|
|
13473
13406
|
* assistant message. Polls /messages/since (server-clock anchored via
|
|
13474
13407
|
* X-Server-Now) until the persisted assistant message lands or the bounded
|
|
13475
13408
|
* budget is exhausted. On success it patches `streamResult.message` so the
|
|
13476
|
-
* normal downstream flow surfaces the answer, and replays any persisted
|
|
13477
|
-
*
|
|
13478
|
-
* the
|
|
13409
|
+
* normal downstream flow surfaces the answer, and replays any persisted balance
|
|
13410
|
+
* card. #927 Phase 2: a signable tx is NEVER recovered — the signable payload
|
|
13411
|
+
* rides tool-output-available, which the persisted parts don't reconstruct — so
|
|
13412
|
+
* a recovered turn that ran a signable tool warns to re-run instead of signing.
|
|
13479
13413
|
*/
|
|
13480
|
-
async recoverDisconnectedTurn(streamResult,
|
|
13414
|
+
async recoverDisconnectedTurn(streamResult, onBalanceSummary) {
|
|
13481
13415
|
if (!this.conversationId) return;
|
|
13482
|
-
const
|
|
13483
|
-
const since = serverAnchor ?? new Date(Date.now() - 2e3).toISOString();
|
|
13484
|
-
const replaySignableCards = serverAnchor !== null;
|
|
13416
|
+
const since = serverNowToIso(streamResult.serverNow) ?? new Date(Date.now() - 2e3).toISOString();
|
|
13485
13417
|
let cursor;
|
|
13486
13418
|
let authRecovered = false;
|
|
13487
13419
|
for (let attempt = 0; attempt < this.recoveryMaxPolls; attempt++) {
|
|
@@ -13502,13 +13434,13 @@ var AgentSession = class {
|
|
|
13502
13434
|
continue;
|
|
13503
13435
|
}
|
|
13504
13436
|
if (resp.cursor) cursor = resp.cursor;
|
|
13505
|
-
const assistant = [...resp.messages].reverse().find((m) => m.role === "assistant" && (!!m.content ||
|
|
13437
|
+
const assistant = [...resp.messages].reverse().find((m) => m.role === "assistant" && (!!m.content || hasSignableToolPart(m.parts)));
|
|
13506
13438
|
if (assistant) {
|
|
13507
13439
|
if (this.config.verbose) {
|
|
13508
13440
|
process.stderr.write(`[session] recovered assistant message after ${attempt + 1} poll(s)
|
|
13509
13441
|
`);
|
|
13510
13442
|
}
|
|
13511
|
-
this.applyRecoveredMessage(assistant, streamResult,
|
|
13443
|
+
this.applyRecoveredMessage(assistant, streamResult, onBalanceSummary);
|
|
13512
13444
|
return;
|
|
13513
13445
|
}
|
|
13514
13446
|
await this.recoverySleep();
|
|
@@ -13596,38 +13528,30 @@ var AgentSession = class {
|
|
|
13596
13528
|
}
|
|
13597
13529
|
/**
|
|
13598
13530
|
* Fold a recovered assistant message back into the live stream result: the
|
|
13599
|
-
* authoritative message wins over any partial deltas, and any persisted
|
|
13600
|
-
*
|
|
13601
|
-
*
|
|
13602
|
-
*
|
|
13603
|
-
*
|
|
13604
|
-
* recovery anchor was the local-clock fallback (no X-Server-Now), where a
|
|
13605
|
-
* recovered card cannot be proven to belong to the current turn. See
|
|
13606
|
-
* recoverDisconnectedTurn — a stale tx_ready must never reach the signer.
|
|
13531
|
+
* authoritative message wins over any partial deltas, and any persisted balance
|
|
13532
|
+
* card is replayed. #927 Phase 2: a signable transaction is NEVER recovered —
|
|
13533
|
+
* the signable payload rides tool-output-available, which the persisted parts
|
|
13534
|
+
* do not reconstruct — so a recovered turn that ran a signable tool warns the
|
|
13535
|
+
* user to re-run rather than signing (fail-CLOSED, never a wrong sign).
|
|
13607
13536
|
*/
|
|
13608
|
-
applyRecoveredMessage(msg, streamResult,
|
|
13537
|
+
applyRecoveredMessage(msg, streamResult, onBalanceSummary) {
|
|
13609
13538
|
streamResult.message = msg;
|
|
13610
|
-
let
|
|
13611
|
-
let signableFlatToolPart;
|
|
13539
|
+
let signableToolPart;
|
|
13612
13540
|
for (const part of msg.parts ?? []) {
|
|
13613
13541
|
if (part.type === "data-balance_summary" && part.data) {
|
|
13614
13542
|
onBalanceSummary?.(part.data);
|
|
13615
13543
|
continue;
|
|
13616
13544
|
}
|
|
13617
|
-
if (replaySignableCards && part.type === "data-tx_ready" && part.data && typeof part.data === "object") {
|
|
13618
|
-
const tx = part.data;
|
|
13619
|
-
streamResult.transactions.push(tx);
|
|
13620
|
-
onTxReady?.(tx);
|
|
13621
|
-
replayedTxReady = true;
|
|
13622
|
-
}
|
|
13623
13545
|
if (typeof part.type === "string" && part.type.startsWith("tool-")) {
|
|
13624
13546
|
const toolName = part.type.slice("tool-".length);
|
|
13625
|
-
if (CLI_SIGNABLE_FLAT_TOOLS.has(toolName)
|
|
13547
|
+
if (CLI_SIGNABLE_FLAT_TOOLS.has(toolName) || CLI_SIGNABLE_PREP_TOOLS.has(toolName)) {
|
|
13548
|
+
signableToolPart = toolName;
|
|
13549
|
+
}
|
|
13626
13550
|
}
|
|
13627
13551
|
}
|
|
13628
|
-
if (
|
|
13552
|
+
if (signableToolPart) {
|
|
13629
13553
|
process.stderr.write(
|
|
13630
|
-
`[session][recovery] recovered turn ran signable tool '${
|
|
13554
|
+
`[session][recovery] recovered turn ran signable tool '${signableToolPart}', whose signable output rides tool-output-available. The recovery path does not reconstruct that candidate, so NO transaction was signed this turn \u2014 re-run the request to sign.
|
|
13631
13555
|
`
|
|
13632
13556
|
);
|
|
13633
13557
|
}
|
|
@@ -13795,8 +13719,12 @@ function serverNowToIso(serverNow) {
|
|
|
13795
13719
|
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
13796
13720
|
return new Date(ms).toISOString();
|
|
13797
13721
|
}
|
|
13798
|
-
function
|
|
13799
|
-
return !!parts?.some((p) =>
|
|
13722
|
+
function hasSignableToolPart(parts) {
|
|
13723
|
+
return !!parts?.some((p) => {
|
|
13724
|
+
if (typeof p.type !== "string" || !p.type.startsWith("tool-")) return false;
|
|
13725
|
+
const toolName = p.type.slice("tool-".length);
|
|
13726
|
+
return CLI_SIGNABLE_FLAT_TOOLS.has(toolName) || CLI_SIGNABLE_PREP_TOOLS.has(toolName);
|
|
13727
|
+
});
|
|
13800
13728
|
}
|
|
13801
13729
|
function isAuthError(err) {
|
|
13802
13730
|
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
@@ -14280,6 +14208,7 @@ function outputAskError(wantsJson, message, code, conversationId, result) {
|
|
|
14280
14208
|
if (result?.transactions.length) data.transactions = result.transactions;
|
|
14281
14209
|
if (result?.toolCalls.length) data.tool_calls = result.toolCalls;
|
|
14282
14210
|
if (result?.response) data.response = result.response;
|
|
14211
|
+
if (result?.outcome) data.outcome = result.outcome;
|
|
14283
14212
|
outputErrorJson({
|
|
14284
14213
|
success: false,
|
|
14285
14214
|
v: 1,
|
|
@@ -14294,6 +14223,11 @@ function outputAskError(wantsJson, message, code, conversationId, result) {
|
|
|
14294
14223
|
function outputAskHuman(result, confirmationRequired, proposed) {
|
|
14295
14224
|
process.stdout.write(`session:${result.sessionId}
|
|
14296
14225
|
`);
|
|
14226
|
+
if (result.outcome && result.outcome.kind !== "success") {
|
|
14227
|
+
const { kind, code } = result.outcome;
|
|
14228
|
+
process.stdout.write(`outcome:${kind}${code ? `:${code}` : ""}
|
|
14229
|
+
`);
|
|
14230
|
+
}
|
|
14297
14231
|
if (confirmationRequired) {
|
|
14298
14232
|
process.stdout.write(`confirmation-required:pass --yes to authorize signing
|
|
14299
14233
|
`);
|
|
@@ -14336,6 +14270,12 @@ function outputAskSuccess(wantsJson, result, conversationId) {
|
|
|
14336
14270
|
tool_calls: result.toolCalls,
|
|
14337
14271
|
transactions: result.transactions,
|
|
14338
14272
|
...result.cards.length > 0 ? { cards: result.cards } : {},
|
|
14273
|
+
// a2a-02: the typed turn ending (success | blocked | refusal | error) at
|
|
14274
|
+
// `data.outcome` — the same relative slot as on the error envelope. Present
|
|
14275
|
+
// only against a backend that honored the advertised turn_outcome surface;
|
|
14276
|
+
// headless callers should branch on this (and the exit code), not `success`
|
|
14277
|
+
// (which stays true for a completed-but-blocked/refused turn).
|
|
14278
|
+
...result.outcome ? { outcome: result.outcome } : {},
|
|
14339
14279
|
...confirmationRequired ? { confirmation_required: true } : {},
|
|
14340
14280
|
...proposed ? { proposed } : {}
|
|
14341
14281
|
});
|
|
@@ -14343,6 +14283,21 @@ function outputAskSuccess(wantsJson, result, conversationId) {
|
|
|
14343
14283
|
}
|
|
14344
14284
|
outputAskHuman(result, confirmationRequired, proposed);
|
|
14345
14285
|
}
|
|
14286
|
+
function outcomeToExitCode(outcome) {
|
|
14287
|
+
switch (outcome?.kind) {
|
|
14288
|
+
case "blocked":
|
|
14289
|
+
return 10 /* AGENT_TURN_BLOCKED */;
|
|
14290
|
+
case "refusal":
|
|
14291
|
+
return 11 /* AGENT_TURN_REFUSAL */;
|
|
14292
|
+
case "error":
|
|
14293
|
+
return 1 /* USAGE */;
|
|
14294
|
+
// 1 — generic failure; a stream error-frame path sets a more specific code first
|
|
14295
|
+
case "success":
|
|
14296
|
+
return 0 /* SUCCESS */;
|
|
14297
|
+
default:
|
|
14298
|
+
return void 0;
|
|
14299
|
+
}
|
|
14300
|
+
}
|
|
14346
14301
|
async function executeAgentAsk(ctx2, message, options) {
|
|
14347
14302
|
setSilentMode(true);
|
|
14348
14303
|
const originalConsoleLog = console.log;
|
|
@@ -14376,6 +14331,7 @@ async function executeAgentAsk(ctx2, message, options) {
|
|
|
14376
14331
|
exitCode = agentErrorCodeToExitCode(result.error.code);
|
|
14377
14332
|
outputAskError(wantsJson, result.error.message, result.error.code, conversationId, result);
|
|
14378
14333
|
} else {
|
|
14334
|
+
exitCode = outcomeToExitCode(result.outcome) ?? 0;
|
|
14379
14335
|
outputAskSuccess(wantsJson, result, conversationId);
|
|
14380
14336
|
}
|
|
14381
14337
|
} catch (err) {
|
|
@@ -14494,7 +14450,7 @@ var cachedVersion = null;
|
|
|
14494
14450
|
function getVersion() {
|
|
14495
14451
|
if (cachedVersion) return cachedVersion;
|
|
14496
14452
|
if (true) {
|
|
14497
|
-
cachedVersion = "2.
|
|
14453
|
+
cachedVersion = "2.18.6";
|
|
14498
14454
|
return cachedVersion;
|
|
14499
14455
|
}
|
|
14500
14456
|
try {
|
|
@@ -16886,19 +16842,27 @@ program.command("broadcast").description("Broadcast a pre-signed raw transaction
|
|
|
16886
16842
|
});
|
|
16887
16843
|
})
|
|
16888
16844
|
);
|
|
16889
|
-
program.command("tx-status").description("Check the status of a transaction (polls until confirmed)").requiredOption("--chain <chain>", "Target blockchain").requiredOption("--tx-hash <hash>", "Transaction hash to check").option("--no-wait", "Return immediately without waiting for confirmation").addHelpText(
|
|
16845
|
+
program.command("tx-status").description("Check the status of a transaction (polls until confirmed)").requiredOption("--chain <chain>", "Target blockchain").requiredOption("--tx-hash <hash>", "Transaction hash to check").option("--no-wait", "Return immediately without waiting for confirmation").option("--timeout <seconds>", "Max seconds to poll before giving up (default 120)").addHelpText(
|
|
16890
16846
|
"after",
|
|
16891
16847
|
`
|
|
16892
16848
|
Examples:
|
|
16893
16849
|
vultisig tx-status --chain Ethereum --tx-hash 0xabc...
|
|
16850
|
+
vultisig tx-status --chain Ethereum --tx-hash 0xabc... --timeout 300
|
|
16894
16851
|
vultisig tx-status --chain Bitcoin --tx-hash abc... --no-wait --output json`
|
|
16895
16852
|
).action(
|
|
16896
16853
|
withExit(async (options) => {
|
|
16897
16854
|
const context = await init(program.opts().vault);
|
|
16855
|
+
const timeoutSec = options.timeout !== void 0 ? Number(options.timeout) : void 0;
|
|
16856
|
+
if (timeoutSec !== void 0 && (!Number.isFinite(timeoutSec) || timeoutSec < 0)) {
|
|
16857
|
+
throw new InvalidInputError(
|
|
16858
|
+
`Invalid --timeout: "${options.timeout}" (expected a non-negative number of seconds)`
|
|
16859
|
+
);
|
|
16860
|
+
}
|
|
16898
16861
|
await executeTxStatus(context, {
|
|
16899
16862
|
chain: findChainByName(options.chain) || options.chain,
|
|
16900
16863
|
txHash: options.txHash,
|
|
16901
|
-
noWait: !options.wait
|
|
16864
|
+
noWait: !options.wait,
|
|
16865
|
+
timeoutSec
|
|
16902
16866
|
});
|
|
16903
16867
|
})
|
|
16904
16868
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vultisig/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.6",
|
|
4
4
|
"description": "The self-custody MPC wallet CLI for AI coding agents (Claude Code, Cursor, OpenCode). Natural-language agent mode, 36+ chains, DKLS23 threshold signatures. Seedless.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"@napi-rs/keyring": "^1.3.0",
|
|
75
75
|
"@noble/hashes": "^2.2.0",
|
|
76
76
|
"@vultisig/client-shared": "^0.2.17",
|
|
77
|
-
"@vultisig/core-chain": "^2.23.
|
|
78
|
-
"@vultisig/rujira": "^
|
|
79
|
-
"@vultisig/sdk": "^2.
|
|
77
|
+
"@vultisig/core-chain": "^2.23.3",
|
|
78
|
+
"@vultisig/rujira": "^51.0.0",
|
|
79
|
+
"@vultisig/sdk": "^2.18.6",
|
|
80
80
|
"chalk": "^5.6.2",
|
|
81
81
|
"cli-table3": "^0.6.5",
|
|
82
82
|
"commander": "^15.0.0",
|