@thirdfy/agent-cli 0.1.8 → 0.1.10
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 +9 -0
- package/README.md +35 -0
- package/bin/thirdfy-agent.mjs +259 -15
- package/package.json +3 -2
- package/src/utils/cli/intentNormalization.cjs +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.1.9] - 2026-04-08
|
|
8
|
+
|
|
9
|
+
**npm:** [`@thirdfy/agent-cli@0.1.9`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.9)
|
|
10
|
+
**Git tag:** [`v0.1.9`](https://github.com/thirdfy/agent-cli/releases/tag/v0.1.9)
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Self preflight routing for swap-like actions:** `preflight --run-mode self` now selects the validation route by action capability. When an action advertises `supportsExecuteIntent=false` and `supportsBuildTx=true`, preflight uses `build-tx` directly instead of forcing execute-intent validation.
|
|
15
|
+
|
|
7
16
|
## [0.1.8] - 2026-04-07
|
|
8
17
|
|
|
9
18
|
**npm:** [`@thirdfy/agent-cli@0.1.8`](https://www.npmjs.com/package/@thirdfy/agent-cli/v/0.1.8)
|
package/README.md
CHANGED
|
@@ -16,8 +16,13 @@ If you want the full developer docs, start here:
|
|
|
16
16
|
- [API Reference home](https://docs.thirdfy.com/api)
|
|
17
17
|
- [LLM docs map (llms.txt)](https://docs.thirdfy.com/llms.txt)
|
|
18
18
|
- [LLM full API snapshot (llms-full.txt)](https://docs.thirdfy.com/llms-full.txt)
|
|
19
|
+
- [Provider docs index (repo)](./docs/providers/index.md)
|
|
20
|
+
- [Action inventory and compatibility](./docs/action-inventory-and-compatibility.md)
|
|
21
|
+
- [Command reference](./docs/command-reference.md)
|
|
19
22
|
- [DogeOS quickstart (repo)](./docs/dogeos-quickstart.md)
|
|
23
|
+
- [Bitfinex CEX workflow (repo)](./docs/bitfinex-cex-workflow.md)
|
|
20
24
|
- [Action onboarding standard](./docs/action-onboarding-standard.md)
|
|
25
|
+
- [Provider docs governance model](./docs/provider-docs-governance.md)
|
|
21
26
|
- [Skill integration RFC](./docs/skill-integration-rfc.md)
|
|
22
27
|
|
|
23
28
|
## Install
|
|
@@ -33,6 +38,13 @@ Run without global install:
|
|
|
33
38
|
npx @thirdfy/agent-cli --help
|
|
34
39
|
```
|
|
35
40
|
|
|
41
|
+
## What's new in 0.1.8
|
|
42
|
+
|
|
43
|
+
- Managed custodial lifecycle parity validation is now first-class in CLI E2E/certification.
|
|
44
|
+
- `doctor self` runs as a local diagnostic command without capabilities negotiation.
|
|
45
|
+
- Self-exec effect-check fallback payloads are hardened for deterministic automation parsing.
|
|
46
|
+
- Managed wallet aliases (`managed wallet init/grant`) are wired directly to custodial delegation handlers.
|
|
47
|
+
|
|
36
48
|
## Quick start
|
|
37
49
|
|
|
38
50
|
```bash
|
|
@@ -101,6 +113,9 @@ thirdfy-agent agent register --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN
|
|
|
101
113
|
- `run` auto-generates `idempotencyKey` by default when not provided.
|
|
102
114
|
- For deterministic retries across workers/restarts, pass your own stable `--idempotency-key`.
|
|
103
115
|
- `preflight` does not force idempotency keys.
|
|
116
|
+
- `preflight --run-mode self` is capability-aware:
|
|
117
|
+
- if an action supports execute-intent validation, CLI uses `executionLane=validation_only` before building tx;
|
|
118
|
+
- if an action advertises `supportsExecuteIntent=false` and `supportsBuildTx=true`, CLI routes preflight directly to `build-tx`.
|
|
104
119
|
|
|
105
120
|
## Run modes and profiles
|
|
106
121
|
|
|
@@ -110,6 +125,12 @@ thirdfy-agent agent register --owner-session-token "$THIRDFY_OWNER_SESSION_TOKEN
|
|
|
110
125
|
- `--run-mode self` -> self-custody rail (`build-tx` flow; default for `profile=personal`)
|
|
111
126
|
- `--run-mode hybrid` -> self-custody + governance mirror metadata (default for `profile=builder`)
|
|
112
127
|
|
|
128
|
+
| Goal | Recommended mode | Why |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| BYOW signing and local custody | `self` | Keeps signing and broadcast on your wallet path. |
|
|
131
|
+
| Managed delegated execution | `thirdfy` | Uses delegated governance rail for hosted execution. |
|
|
132
|
+
| Self execution plus governance mirror checks | `hybrid` | Preserves local execution while emitting managed-lane mirrors. |
|
|
133
|
+
|
|
113
134
|
Auth expectations by mode:
|
|
114
135
|
|
|
115
136
|
- `self`: requires execution identity (`agentApiKey`) today; keyless self lane is blocked with deterministic `SELF_OPEN_DISABLED`.
|
|
@@ -199,6 +220,14 @@ Jeff-friendly aliases map directly to core commands:
|
|
|
199
220
|
- `thirdfy-agent jeff status --intent-id ...` -> alias of `intent-status`
|
|
200
221
|
- `thirdfy-agent prompt "<text>"` -> captures prompt intent and suggests deterministic execution commands
|
|
201
222
|
|
|
223
|
+
## Provider-specific workflows
|
|
224
|
+
|
|
225
|
+
Provider and venue guides are kept outside `README` so this page stays stable as integrations grow.
|
|
226
|
+
|
|
227
|
+
- Provider index: [`docs/providers/index.md`](./docs/providers/index.md)
|
|
228
|
+
- Bitfinex runbook: [`docs/providers/bitfinex.md`](./docs/providers/bitfinex.md)
|
|
229
|
+
- DogeOS runbook: [`docs/providers/dogeos.md`](./docs/providers/dogeos.md)
|
|
230
|
+
|
|
202
231
|
## Command groups
|
|
203
232
|
|
|
204
233
|
- Discovery: `catalogs list`, `actions`
|
|
@@ -210,6 +239,12 @@ Jeff-friendly aliases map directly to core commands:
|
|
|
210
239
|
- Delegation execution helpers: `delegation balance`, `delegation redeem`
|
|
211
240
|
- Account: `credits balance`
|
|
212
241
|
|
|
242
|
+
## Where to find what is supported
|
|
243
|
+
|
|
244
|
+
- Full command groups and usage map: [`docs/command-reference.md`](./docs/command-reference.md)
|
|
245
|
+
- Live action inventory in your environment: [`docs/action-inventory-and-compatibility.md`](./docs/action-inventory-and-compatibility.md)
|
|
246
|
+
- Provider runbooks and provider status: [`docs/providers/index.md`](./docs/providers/index.md)
|
|
247
|
+
|
|
213
248
|
## Delegation lifecycle (canonical)
|
|
214
249
|
|
|
215
250
|
Use `create` + `activate` as the default delegated execution lifecycle:
|
package/bin/thirdfy-agent.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { randomUUID } from 'crypto';
|
|
3
|
+
import { createHmac, randomUUID } from 'crypto';
|
|
4
4
|
import { spawnSync } from 'child_process';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import os from 'os';
|
|
@@ -18,6 +18,11 @@ const {
|
|
|
18
18
|
const DEFAULT_API_BASE = 'https://api.thirdfy.com';
|
|
19
19
|
const DEFAULT_TIMEOUT_MS = 30000;
|
|
20
20
|
const DEFAULT_CATALOG_CACHE_TTL_MS = 60_000;
|
|
21
|
+
const DEFAULT_BITFINEX_BASE_URL = 'https://api.bitfinex.com';
|
|
22
|
+
const BITFINEX_REQUIRED_SCOPE_PERMISSIONS = {
|
|
23
|
+
funding: { read: 1, write: 1 },
|
|
24
|
+
wallets: { read: 1, write: 1 },
|
|
25
|
+
};
|
|
21
26
|
const RUN_MODES = ['thirdfy', 'self', 'hybrid'];
|
|
22
27
|
const EFFECT_CHECK_MODES = ['strict', 'relaxed', 'off'];
|
|
23
28
|
const PROFILE_DEFAULTS = {
|
|
@@ -52,6 +57,7 @@ main().catch((error) => {
|
|
|
52
57
|
const payload = error?.payload && typeof error.payload === 'object' ? error.payload : {};
|
|
53
58
|
const blockedReason = String(payload.blockedReason || payload.error || '').trim() || undefined;
|
|
54
59
|
const blockedStage = String(payload.blockedStage || '').trim() || undefined;
|
|
60
|
+
const onboardingHint = buildTopLevelOnboardingHint(payload);
|
|
55
61
|
printEnvelope({
|
|
56
62
|
success: false,
|
|
57
63
|
code: statusCode ? 'API_REQUEST_FAILED' : 'CLI_UNHANDLED_ERROR',
|
|
@@ -60,6 +66,7 @@ main().catch((error) => {
|
|
|
60
66
|
statusCode,
|
|
61
67
|
blockedReason,
|
|
62
68
|
blockedStage,
|
|
69
|
+
onboardingHint,
|
|
63
70
|
payload,
|
|
64
71
|
},
|
|
65
72
|
meta: { version: CLI_VERSION, apiBase: context.apiBase },
|
|
@@ -67,6 +74,22 @@ main().catch((error) => {
|
|
|
67
74
|
process.exit(1);
|
|
68
75
|
});
|
|
69
76
|
|
|
77
|
+
function buildTopLevelOnboardingHint(payload) {
|
|
78
|
+
const reason = String(payload?.blockedReason || payload?.error || '').trim();
|
|
79
|
+
if (!reason) return null;
|
|
80
|
+
if (
|
|
81
|
+
['CEX_CREDENTIAL_NOT_FOUND', 'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT', 'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED'].includes(
|
|
82
|
+
reason
|
|
83
|
+
)
|
|
84
|
+
) {
|
|
85
|
+
return {
|
|
86
|
+
type: 'bitfinex_credentials',
|
|
87
|
+
command: buildBitfinexOnboardingCommand(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
async function main() {
|
|
71
94
|
const profileState = resolveProfileState(globalFlags);
|
|
72
95
|
context.profile = profileState.profile;
|
|
@@ -517,11 +540,16 @@ async function commandPreflight(ctx, flags, capabilities) {
|
|
|
517
540
|
prepareActionParamsForFlags(flags, resolved.resolvedAction);
|
|
518
541
|
const runMode = getEffectiveRunMode(ctx, flags);
|
|
519
542
|
enforceChainCompatibility(capabilities, flags, runMode);
|
|
520
|
-
const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved
|
|
521
|
-
const normalized = backendResult.normalized
|
|
543
|
+
const backendResult = await runPreflightByMode(runMode, ctx, flags, resolved);
|
|
544
|
+
const normalized = applyExecutionFallbackHints(backendResult.normalized, {
|
|
545
|
+
flags,
|
|
546
|
+
runMode,
|
|
547
|
+
resolvedAction: resolved.resolvedAction,
|
|
548
|
+
});
|
|
549
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
522
550
|
printEnvelope({
|
|
523
551
|
success: normalized.success,
|
|
524
|
-
code: normalized.success ? 'PREFLIGHT_OK' : 'PREFLIGHT_FAILED',
|
|
552
|
+
code: normalized.success ? 'PREFLIGHT_OK' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'PREFLIGHT_FAILED',
|
|
525
553
|
message: backendResult.message,
|
|
526
554
|
data: normalized,
|
|
527
555
|
meta: {
|
|
@@ -529,6 +557,7 @@ async function commandPreflight(ctx, flags, capabilities) {
|
|
|
529
557
|
catalog: flags.catalog || null,
|
|
530
558
|
requestedAction: resolved.requestedAction,
|
|
531
559
|
resolvedAction: resolved.resolvedAction,
|
|
560
|
+
preflightRoute: backendResult.route,
|
|
532
561
|
runMode,
|
|
533
562
|
profile: ctx.profile || null,
|
|
534
563
|
swapInput: flags.__swapInput || null,
|
|
@@ -547,7 +576,11 @@ async function commandRun(ctx, flags, capabilities) {
|
|
|
547
576
|
const backendResult = await runExecutionByMode(runMode, ctx, flags, resolved, {
|
|
548
577
|
skipPreflight,
|
|
549
578
|
});
|
|
550
|
-
const normalized = backendResult.normalized
|
|
579
|
+
const normalized = applyExecutionFallbackHints(backendResult.normalized, {
|
|
580
|
+
flags,
|
|
581
|
+
runMode,
|
|
582
|
+
resolvedAction: resolved.resolvedAction,
|
|
583
|
+
});
|
|
551
584
|
if (runMode === 'self' && parseBooleanFlag(flags.broadcast, false) && normalized?.success) {
|
|
552
585
|
const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, normalized);
|
|
553
586
|
printEnvelope({
|
|
@@ -924,8 +957,9 @@ async function commandDelegationRedeem(ctx, flags, capabilities) {
|
|
|
924
957
|
|
|
925
958
|
async function commandCredentialsUpsert(ctx, flags, capabilities) {
|
|
926
959
|
const authToken = getAuthToken(flags, 'Missing --auth-token (or THIRDFY_AUTH_TOKEN) for credentials upsert');
|
|
960
|
+
const venue = requireFlag(flags, 'venue', 'Missing --venue');
|
|
927
961
|
const payload = {
|
|
928
|
-
venue
|
|
962
|
+
venue,
|
|
929
963
|
apiKey: requireFlag(flags, 'apiKey', 'Missing --api-key'),
|
|
930
964
|
apiSecret: requireFlag(flags, 'apiSecret', 'Missing --api-secret'),
|
|
931
965
|
credentialType: String(flags.credentialType || 'api_key'),
|
|
@@ -936,6 +970,19 @@ async function commandCredentialsUpsert(ctx, flags, capabilities) {
|
|
|
936
970
|
if (flags.policy) payload.policy = parseJsonFlag(flags, 'policy');
|
|
937
971
|
if (flags.agentKey) payload.agentKey = String(flags.agentKey).trim();
|
|
938
972
|
if (flags.chainId) payload.chainId = Number(flags.chainId);
|
|
973
|
+
const probeEnabled =
|
|
974
|
+
String(venue).trim().toLowerCase() === 'bitfinex' &&
|
|
975
|
+
!parseBooleanFlag(flags.skipPermissionsProbe, false) &&
|
|
976
|
+
parseBooleanFlag(flags.verifyPermissions, true);
|
|
977
|
+
let permissionProbe = null;
|
|
978
|
+
if (probeEnabled) {
|
|
979
|
+
permissionProbe = await probeBitfinexPermissions({
|
|
980
|
+
apiKey: payload.apiKey,
|
|
981
|
+
apiSecret: payload.apiSecret,
|
|
982
|
+
timeoutMs: ctx.timeoutMs,
|
|
983
|
+
baseUrl: String(flags.bitfinexBaseUrl || process.env.BITFINEX_BASE_URL || DEFAULT_BITFINEX_BASE_URL),
|
|
984
|
+
});
|
|
985
|
+
}
|
|
939
986
|
|
|
940
987
|
const response = await requestWithRouteFallback(ctx, {
|
|
941
988
|
method: 'POST',
|
|
@@ -952,6 +999,7 @@ async function commandCredentialsUpsert(ctx, flags, capabilities) {
|
|
|
952
999
|
data: response,
|
|
953
1000
|
meta: {
|
|
954
1001
|
apiBase: ctx.apiBase,
|
|
1002
|
+
permissionProbe,
|
|
955
1003
|
capabilitiesVersion: capabilities?.contractVersion || null,
|
|
956
1004
|
},
|
|
957
1005
|
});
|
|
@@ -1710,13 +1758,82 @@ function enforceChainCompatibility(capabilities, flags, runMode) {
|
|
|
1710
1758
|
}
|
|
1711
1759
|
}
|
|
1712
1760
|
|
|
1713
|
-
|
|
1761
|
+
function shouldUseBuildTxExecution(runMode, resolved) {
|
|
1762
|
+
if (runMode !== 'self' && runMode !== 'hybrid') return false;
|
|
1763
|
+
const meta = resolved?.resolvedActionMeta || null;
|
|
1764
|
+
if (!meta || typeof meta !== 'object') return true;
|
|
1765
|
+
if (meta.supportsBuildTx === false) return false;
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
function buildBitfinexOnboardingCommand() {
|
|
1770
|
+
return [
|
|
1771
|
+
'thirdfy-agent credentials upsert',
|
|
1772
|
+
'--auth-token "$THIRDFY_AUTH_TOKEN"',
|
|
1773
|
+
'--venue bitfinex',
|
|
1774
|
+
'--api-key "<BITFINEX_API_KEY>"',
|
|
1775
|
+
'--api-secret "<BITFINEX_API_SECRET>"',
|
|
1776
|
+
'--verify-permissions',
|
|
1777
|
+
'--json',
|
|
1778
|
+
].join(' ');
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedAction }) {
|
|
1782
|
+
if (!normalized || typeof normalized !== 'object') return normalized;
|
|
1783
|
+
const next = { ...normalized };
|
|
1784
|
+
const reasons = new Set();
|
|
1785
|
+
const topBlockedReason = String(next.blockedReason || '').trim();
|
|
1786
|
+
if (topBlockedReason) reasons.add(topBlockedReason);
|
|
1787
|
+
const blockedByReason = next.blockedByReason && typeof next.blockedByReason === 'object' ? next.blockedByReason : {};
|
|
1788
|
+
for (const key of Object.keys(blockedByReason)) {
|
|
1789
|
+
if (String(key || '').trim()) reasons.add(String(key).trim());
|
|
1790
|
+
}
|
|
1791
|
+
const resolvedActionLower = String(resolvedAction || '').trim().toLowerCase();
|
|
1792
|
+
const looksBitfinexAction = resolvedActionLower.includes('bitfinex');
|
|
1793
|
+
const cexCredentialMissingSignals = new Set([
|
|
1794
|
+
'CEX_CREDENTIAL_NOT_FOUND',
|
|
1795
|
+
'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
|
|
1796
|
+
'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
|
|
1797
|
+
]);
|
|
1798
|
+
const hasMissingCredentialSignal = Array.from(reasons).some((code) => cexCredentialMissingSignals.has(code));
|
|
1799
|
+
if (looksBitfinexAction && hasMissingCredentialSignal) {
|
|
1800
|
+
next.onboardingHint = {
|
|
1801
|
+
type: 'bitfinex_credentials',
|
|
1802
|
+
message:
|
|
1803
|
+
'Bitfinex credentials are missing for this delegated account. Onboard credentials first, then retry the same command.',
|
|
1804
|
+
command: buildBitfinexOnboardingCommand(),
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
if (
|
|
1808
|
+
looksBitfinexAction &&
|
|
1809
|
+
runMode !== 'thirdfy' &&
|
|
1810
|
+
(topBlockedReason === 'SELF_MODE_REQUIRES_BUILD_TX' || topBlockedReason === 'HYBRID_MODE_REQUIRES_BUILD_TX')
|
|
1811
|
+
) {
|
|
1812
|
+
next.onboardingHint = {
|
|
1813
|
+
...(next.onboardingHint || {}),
|
|
1814
|
+
runMode: 'thirdfy',
|
|
1815
|
+
runModeMessage: 'Bitfinex actions require delegated execute-intent rail. Re-run with --run-mode thirdfy.',
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
return next;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function shouldUseBuildTxPreflight(runMode, resolved) {
|
|
1822
|
+
if (runMode !== 'self') return false;
|
|
1823
|
+
const meta = resolved?.resolvedActionMeta || null;
|
|
1824
|
+
if (!meta || typeof meta !== 'object') return false;
|
|
1825
|
+
return meta.supportsExecuteIntent === false && meta.supportsBuildTx === true;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
async function runPreflightByMode(runMode, ctx, flags, resolved) {
|
|
1829
|
+
const resolvedAction = resolved.resolvedAction;
|
|
1714
1830
|
if (runMode === 'self') {
|
|
1831
|
+
const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
|
|
1715
1832
|
const normalized = await executeSelfRun(
|
|
1716
1833
|
ctx,
|
|
1717
1834
|
flags,
|
|
1718
|
-
|
|
1719
|
-
{ skipPreflight:
|
|
1835
|
+
resolved,
|
|
1836
|
+
{ skipPreflight: route === 'build_tx' }
|
|
1720
1837
|
);
|
|
1721
1838
|
const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
1722
1839
|
return {
|
|
@@ -1725,6 +1842,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
|
|
|
1725
1842
|
: normalized.success
|
|
1726
1843
|
? 'Self preflight completed (unsigned transaction ready)'
|
|
1727
1844
|
: 'Self preflight failed',
|
|
1845
|
+
route,
|
|
1728
1846
|
normalized,
|
|
1729
1847
|
};
|
|
1730
1848
|
}
|
|
@@ -1745,6 +1863,7 @@ async function runPreflightByMode(runMode, ctx, flags, resolvedAction) {
|
|
|
1745
1863
|
: runMode === 'hybrid'
|
|
1746
1864
|
? 'Hybrid mirror preflight failed'
|
|
1747
1865
|
: 'Preflight failed',
|
|
1866
|
+
route: 'execute_intent_validation',
|
|
1748
1867
|
normalized,
|
|
1749
1868
|
};
|
|
1750
1869
|
}
|
|
@@ -1831,6 +1950,7 @@ async function executeThirdfyRun(ctx, flags, resolved, options) {
|
|
|
1831
1950
|
|
|
1832
1951
|
async function executeSelfRun(ctx, flags, resolved, options) {
|
|
1833
1952
|
const skipPreflight = Boolean(options?.skipPreflight);
|
|
1953
|
+
const blockedReasonCode = String(options?.blockedReasonCode || 'SELF_MODE_REQUIRES_BUILD_TX');
|
|
1834
1954
|
if (!skipPreflight) {
|
|
1835
1955
|
const preflightPayload = buildIntentPayload(flags, {
|
|
1836
1956
|
validationOnly: true,
|
|
@@ -1849,6 +1969,17 @@ async function executeSelfRun(ctx, flags, resolved, options) {
|
|
|
1849
1969
|
return preflightNormalized;
|
|
1850
1970
|
}
|
|
1851
1971
|
}
|
|
1972
|
+
if (!shouldUseBuildTxExecution(options?.runMode || 'self', resolved)) {
|
|
1973
|
+
const normalized = normalizeIntentResponse({
|
|
1974
|
+
success: false,
|
|
1975
|
+
status: 'failed',
|
|
1976
|
+
blockedReason: blockedReasonCode,
|
|
1977
|
+
blockedStage: 'routing',
|
|
1978
|
+
error: 'Selected action does not support build-tx self lane. Use --run-mode thirdfy for delegated CEX execution.',
|
|
1979
|
+
});
|
|
1980
|
+
normalized.preflightBlocked = true;
|
|
1981
|
+
return normalized;
|
|
1982
|
+
}
|
|
1852
1983
|
const buildTxPayload = buildBuildTxPayload(flags, resolved.resolvedAction);
|
|
1853
1984
|
const buildTx = await apiPost(ctx, '/api/v1/agent/build-tx', buildTxPayload);
|
|
1854
1985
|
return normalizeIntentResponse({
|
|
@@ -1861,7 +1992,11 @@ async function executeSelfRun(ctx, flags, resolved, options) {
|
|
|
1861
1992
|
}
|
|
1862
1993
|
|
|
1863
1994
|
async function executeHybridRun(ctx, flags, resolved, options) {
|
|
1864
|
-
const selfResult = await executeSelfRun(ctx, flags, resolved,
|
|
1995
|
+
const selfResult = await executeSelfRun(ctx, flags, resolved, {
|
|
1996
|
+
...(options || {}),
|
|
1997
|
+
runMode: 'hybrid',
|
|
1998
|
+
blockedReasonCode: 'HYBRID_MODE_REQUIRES_BUILD_TX',
|
|
1999
|
+
});
|
|
1865
2000
|
if (!selfResult.success) return selfResult;
|
|
1866
2001
|
|
|
1867
2002
|
const mirrorPayload = buildIntentPayload(flags, {
|
|
@@ -2006,18 +2141,19 @@ async function resolveActionSelection(ctx, flags) {
|
|
|
2006
2141
|
.map((action) => ({
|
|
2007
2142
|
key: getActionKey(action),
|
|
2008
2143
|
aliases: getActionAliases(action),
|
|
2144
|
+
action,
|
|
2009
2145
|
}))
|
|
2010
2146
|
.filter((entry) => entry.key);
|
|
2011
2147
|
|
|
2012
2148
|
if (!keyed.length) {
|
|
2013
|
-
return { requestedAction, resolvedAction: requestedAction };
|
|
2149
|
+
return { requestedAction, resolvedAction: requestedAction, resolvedActionMeta: null };
|
|
2014
2150
|
}
|
|
2015
2151
|
|
|
2016
2152
|
// Prefer direct action id matches over alias matches.
|
|
2017
2153
|
// This keeps UX simple for commands like `--action swap` when another action aliases to "swap".
|
|
2018
2154
|
const exactKey = keyed.filter((entry) => entry.key.toLowerCase() === requestedLower);
|
|
2019
2155
|
if (exactKey.length === 1) {
|
|
2020
|
-
return { requestedAction, resolvedAction: exactKey[0].key };
|
|
2156
|
+
return { requestedAction, resolvedAction: exactKey[0].key, resolvedActionMeta: exactKey[0].action || null };
|
|
2021
2157
|
}
|
|
2022
2158
|
if (exactKey.length > 1) {
|
|
2023
2159
|
throw new Error(
|
|
@@ -2029,7 +2165,7 @@ async function resolveActionSelection(ctx, flags) {
|
|
|
2029
2165
|
|
|
2030
2166
|
const exactAlias = keyed.filter((entry) => entry.aliases.some((a) => a.toLowerCase() === requestedLower));
|
|
2031
2167
|
if (exactAlias.length === 1) {
|
|
2032
|
-
return { requestedAction, resolvedAction: exactAlias[0].key };
|
|
2168
|
+
return { requestedAction, resolvedAction: exactAlias[0].key, resolvedActionMeta: exactAlias[0].action || null };
|
|
2033
2169
|
}
|
|
2034
2170
|
if (exactAlias.length > 1) {
|
|
2035
2171
|
throw new Error(
|
|
@@ -2045,7 +2181,7 @@ async function resolveActionSelection(ctx, flags) {
|
|
|
2045
2181
|
entry.aliases.some((a) => a.toLowerCase().includes(requestedLower))
|
|
2046
2182
|
);
|
|
2047
2183
|
if (fuzzy.length === 1) {
|
|
2048
|
-
return { requestedAction, resolvedAction: fuzzy[0].key };
|
|
2184
|
+
return { requestedAction, resolvedAction: fuzzy[0].key, resolvedActionMeta: fuzzy[0].action || null };
|
|
2049
2185
|
}
|
|
2050
2186
|
if (fuzzy.length > 1) {
|
|
2051
2187
|
throw new Error(
|
|
@@ -2441,6 +2577,114 @@ function safeJsonParse(text) {
|
|
|
2441
2577
|
}
|
|
2442
2578
|
}
|
|
2443
2579
|
|
|
2580
|
+
function parseBitfinexPermissionRows(rawData) {
|
|
2581
|
+
const rows = Array.isArray(rawData) ? rawData : [];
|
|
2582
|
+
const parsed = {};
|
|
2583
|
+
for (const row of rows) {
|
|
2584
|
+
if (!Array.isArray(row) || row.length < 3) continue;
|
|
2585
|
+
const scope = String(row[0] || '').trim().toLowerCase();
|
|
2586
|
+
if (!scope) continue;
|
|
2587
|
+
const read = Number(row[1]) === 1 ? 1 : 0;
|
|
2588
|
+
const write = Number(row[2]) === 1 ? 1 : 0;
|
|
2589
|
+
parsed[scope] = { read, write };
|
|
2590
|
+
}
|
|
2591
|
+
return parsed;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
function getMissingBitfinexScopes(scopePermissions) {
|
|
2595
|
+
const missing = [];
|
|
2596
|
+
for (const [scope, required] of Object.entries(BITFINEX_REQUIRED_SCOPE_PERMISSIONS)) {
|
|
2597
|
+
const actual = scopePermissions[scope] || { read: 0, write: 0 };
|
|
2598
|
+
if (actual.read < required.read || actual.write < required.write) {
|
|
2599
|
+
missing.push({
|
|
2600
|
+
scope,
|
|
2601
|
+
required,
|
|
2602
|
+
actual,
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
return missing;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
async function probeBitfinexPermissions({ apiKey, apiSecret, timeoutMs, baseUrl }) {
|
|
2610
|
+
const normalizedBaseUrl = String(baseUrl || DEFAULT_BITFINEX_BASE_URL).trim().replace(/\/+$/, '');
|
|
2611
|
+
const apiPath = '/v2/auth/r/permissions';
|
|
2612
|
+
const nonce = Date.now().toString();
|
|
2613
|
+
const payload = '{}';
|
|
2614
|
+
const signature = createHmac('sha384', String(apiSecret || ''))
|
|
2615
|
+
.update(`/api${apiPath}${nonce}${payload}`)
|
|
2616
|
+
.digest('hex');
|
|
2617
|
+
const controller = new AbortController();
|
|
2618
|
+
const timer = setTimeout(() => controller.abort(), Number(timeoutMs || DEFAULT_TIMEOUT_MS));
|
|
2619
|
+
try {
|
|
2620
|
+
const response = await fetch(`${normalizedBaseUrl}${apiPath}`, {
|
|
2621
|
+
method: 'POST',
|
|
2622
|
+
headers: {
|
|
2623
|
+
'content-type': 'application/json',
|
|
2624
|
+
'bfx-apikey': String(apiKey || ''),
|
|
2625
|
+
'bfx-nonce': nonce,
|
|
2626
|
+
'bfx-signature': signature,
|
|
2627
|
+
},
|
|
2628
|
+
body: payload,
|
|
2629
|
+
signal: controller.signal,
|
|
2630
|
+
});
|
|
2631
|
+
const text = await response.text();
|
|
2632
|
+
const parsed = text ? safeJsonParse(text) : {};
|
|
2633
|
+
const bitfinexApiError =
|
|
2634
|
+
Array.isArray(parsed) && String(parsed[0] || '').trim().toLowerCase() === 'error'
|
|
2635
|
+
? {
|
|
2636
|
+
code: Number(parsed[1]) || null,
|
|
2637
|
+
message: String(parsed[2] || 'Unknown Bitfinex API error'),
|
|
2638
|
+
}
|
|
2639
|
+
: parsed && typeof parsed === 'object' && String(parsed.event || '').trim().toLowerCase() === 'error'
|
|
2640
|
+
? {
|
|
2641
|
+
code: Number(parsed.code) || null,
|
|
2642
|
+
message: String(parsed.msg || parsed.message || 'Unknown Bitfinex API error'),
|
|
2643
|
+
}
|
|
2644
|
+
: null;
|
|
2645
|
+
if (!response.ok) {
|
|
2646
|
+
throw createCliError(
|
|
2647
|
+
'BITFINEX_PERMISSIONS_PROBE_FAILED',
|
|
2648
|
+
`Bitfinex permissions probe failed with HTTP ${response.status}. Verify API key/secret and key validity before onboarding.`,
|
|
2649
|
+
{ statusCode: response.status }
|
|
2650
|
+
);
|
|
2651
|
+
}
|
|
2652
|
+
if (bitfinexApiError) {
|
|
2653
|
+
throw createCliError(
|
|
2654
|
+
'BITFINEX_PERMISSIONS_PROBE_FAILED',
|
|
2655
|
+
`Bitfinex permissions probe failed: ${bitfinexApiError.message}. Verify API key/secret and key validity before onboarding.`,
|
|
2656
|
+
{ statusCode: response.status, bitfinexApiError }
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
const scopePermissions = parseBitfinexPermissionRows(parsed);
|
|
2660
|
+
const missingScopes = getMissingBitfinexScopes(scopePermissions);
|
|
2661
|
+
if (missingScopes.length > 0) {
|
|
2662
|
+
throw createCliError(
|
|
2663
|
+
'BITFINEX_PERMISSION_SCOPE_MISSING',
|
|
2664
|
+
`Bitfinex key is missing required scopes (${missingScopes
|
|
2665
|
+
.map((entry) => entry.scope)
|
|
2666
|
+
.join(', ')}). Enable funding+wallets read/write and retry onboarding.`,
|
|
2667
|
+
{ missingScopes, scopePermissions }
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
return {
|
|
2671
|
+
venue: 'bitfinex',
|
|
2672
|
+
checked: true,
|
|
2673
|
+
scopes: scopePermissions,
|
|
2674
|
+
missingScopes: [],
|
|
2675
|
+
};
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
if (error?.payload) throw error;
|
|
2678
|
+
throw createCliError(
|
|
2679
|
+
'BITFINEX_PERMISSIONS_PROBE_FAILED',
|
|
2680
|
+
`Could not verify Bitfinex key permissions. ${String(error?.message || 'Unknown probe error')}`,
|
|
2681
|
+
{}
|
|
2682
|
+
);
|
|
2683
|
+
} finally {
|
|
2684
|
+
clearTimeout(timer);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2444
2688
|
function printEnvelope(payload) {
|
|
2445
2689
|
if (jsonMode) {
|
|
2446
2690
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
@@ -2478,7 +2722,7 @@ Core commands:
|
|
|
2478
2722
|
thirdfy-agent managed wallet init [--auth-token <token> | --owner-session-token <token>] [--chain-id <id>] [--json]
|
|
2479
2723
|
thirdfy-agent managed wallet grant [--auth-token <token> | --owner-session-token <token>] --agent-key <key> --token-address <addr> --max-usd-per-day <usd> [--chain-id <id>] [--period-duration <sec>] [--expiry-seconds <sec>] [--json]
|
|
2480
2724
|
thirdfy-agent managed run --agent-api-key <key> --action <id> --params <json> [--run-mode thirdfy|hybrid] [--idempotency-key <key>] [--json]
|
|
2481
|
-
thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--json]
|
|
2725
|
+
thirdfy-agent credentials upsert --auth-token <token> --venue <id> --api-key <key> --api-secret <secret> [--verify-permissions] [--skip-permissions-probe] [--bitfinex-base-url <url>] [--json]
|
|
2482
2726
|
thirdfy-agent credentials status --auth-token <token> [--venue <id>] [--json]
|
|
2483
2727
|
thirdfy-agent agent auth challenge --agent-key <key> [--wallet-address <addr>] [--chain-id <id>] [--json]
|
|
2484
2728
|
thirdfy-agent agent auth verify --challenge-id <id> --signature <hex> --agent-key <key> [--wallet-address <addr>] [--json]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thirdfy/agent-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"test": "node --test \"test/**/*.test.cjs\"",
|
|
20
20
|
"lint": "node --check ./bin/thirdfy-agent.mjs",
|
|
21
|
+
"validate:public-docs": "node ./scripts/validate-public-docs.mjs",
|
|
21
22
|
"validate:cli-contract-schemas": "node ./scripts/validate-cli-contract-schemas.mjs",
|
|
22
23
|
"validate:actions": "node ./scripts/e2e/validate-actions.mjs",
|
|
23
24
|
"e2e:base:simulate": "node ./scripts/e2e/base-simulate.mjs",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"e2e:delegation:prod:certify": "node ./scripts/e2e/prod-delegation-certify.mjs",
|
|
31
32
|
"e2e:report:last": "node ./scripts/e2e/report-last.mjs",
|
|
32
33
|
"smoke:cli": "node ./bin/thirdfy-agent.mjs --help && node ./bin/thirdfy-agent.mjs --version --json",
|
|
33
|
-
"prepublishOnly": "npm run validate:cli-contract-schemas && npm run test && npm run smoke:cli"
|
|
34
|
+
"prepublishOnly": "npm run validate:public-docs && npm run validate:cli-contract-schemas && npm run test && npm run smoke:cli"
|
|
34
35
|
},
|
|
35
36
|
"keywords": [
|
|
36
37
|
"thirdfy",
|
|
@@ -33,6 +33,7 @@ function normalizeIntentResponse(response) {
|
|
|
33
33
|
const topLevelGovernanceSignal = String(response?.governanceSignal || '').trim() || null;
|
|
34
34
|
const topLevelDelegationSignal = String(response?.delegationSignal || '').trim() || null;
|
|
35
35
|
const policyEvaluated = response?.policyEvaluated === true;
|
|
36
|
+
const preflightBlocked = response?.preflightBlocked === true;
|
|
36
37
|
return {
|
|
37
38
|
success,
|
|
38
39
|
status: response?.status || (success ? 'queued' : 'failed'),
|
|
@@ -48,6 +49,7 @@ function normalizeIntentResponse(response) {
|
|
|
48
49
|
governanceSignal: topLevelGovernanceSignal,
|
|
49
50
|
delegationSignal: topLevelDelegationSignal,
|
|
50
51
|
policyEvaluated,
|
|
52
|
+
preflightBlocked,
|
|
51
53
|
error: response?.error || null,
|
|
52
54
|
mode: response?.mode || null,
|
|
53
55
|
unsignedTx: response?.unsignedTx || null,
|