@thirdfy/agent-cli 0.4.1 → 0.4.2
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 +20 -0
- package/README.md +4 -5
- package/package.json +3 -3
- package/src/cli/options/index.mjs +11 -11
- package/src/cli/program.mjs +1 -1
- package/src/commands/{agent.mjs → account/agent.mjs} +5 -5
- package/src/commands/{credits.mjs → account/credits.mjs} +13 -15
- package/src/commands/{wallet.mjs → account/wallet.mjs} +29 -33
- package/src/commands/{walletControl.mjs → account/walletControl.mjs} +3 -3
- package/src/commands/discovery.mjs +1 -1
- package/src/commands/execute.mjs +2 -2
- package/src/commands/index.mjs +9 -9
- package/src/commands/{bootstrap.mjs → onboarding/bootstrap.mjs} +8 -8
- package/src/commands/{consoleMandate.mjs → onboarding/consoleMandate.mjs} +2 -2
- package/src/commands/{credentials.mjs → onboarding/credentials.mjs} +6 -6
- package/src/commands/{delegation.mjs → onboarding/delegation.mjs} +5 -5
- package/src/commands/{avantis.mjs → venues/avantis.mjs} +1 -1
- package/src/commands/{condor.mjs → venues/condor.mjs} +4 -4
- package/src/commands/{hummingbot.mjs → venues/hummingbot.mjs} +4 -4
- package/src/commands/{hyperliquid.mjs → venues/hyperliquid.mjs} +1 -1
- package/src/commands/{lighter.mjs → venues/lighter.mjs} +1 -1
- package/src/commands/{polymarket.mjs → venues/polymarket.mjs} +4 -4
- package/src/commands/{venue.mjs → venues/venue.mjs} +3 -3
- package/src/core/earnclawAttribution.mjs +1 -1
- package/src/core/{actionTimeouts.mjs → execution/actionTimeouts.mjs} +2 -2
- package/src/core/{walletExecuteChainId.mjs → execution/walletExecuteChainId.mjs} +1 -1
- package/src/core/http.mjs +1 -1
- package/src/runtime/execution/hostedRuntimeWriteGate.mjs +1 -1
- package/src/runtime/execution/payloads.mjs +4 -4
- package/src/runtime/execution/runners/agentWalletLane.mjs +109 -0
- package/src/runtime/execution/runners/createExecutionRunners.mjs +315 -0
- package/src/runtime/execution/runners/fallbackHints.mjs +54 -0
- package/src/runtime/execution/runners/hybridLane.mjs +88 -0
- package/src/runtime/execution/runners/managedPreflight.mjs +253 -0
- package/src/runtime/execution/runners/managedResponse.mjs +119 -0
- package/src/runtime/execution/runners/selfLane.mjs +232 -0
- package/src/runtime/execution/runners/thirdfyLane.mjs +97 -0
- package/src/runtime/execution/svmLane.mjs +1 -1
- package/src/runtime/handlers.mjs +15 -16
- package/src/runtime/main.mjs +1 -1
- package/src/runtime/risk/notional.mjs +1 -1
- package/src/runtime/validation/actionParamsGate.mjs +1 -1
- package/src/runtime/execution/runners.mjs +0 -1134
- /package/src/cli/options/{agent.mjs → account/agent.mjs} +0 -0
- /package/src/cli/options/{credits.mjs → account/credits.mjs} +0 -0
- /package/src/cli/options/{wallet.mjs → account/wallet.mjs} +0 -0
- /package/src/cli/options/{walletControl.mjs → account/walletControl.mjs} +0 -0
- /package/src/cli/options/{bootstrap.mjs → onboarding/bootstrap.mjs} +0 -0
- /package/src/cli/options/{credentials.mjs → onboarding/credentials.mjs} +0 -0
- /package/src/cli/options/{delegation.mjs → onboarding/delegation.mjs} +0 -0
- /package/src/cli/options/{condor.mjs → venues/condor.mjs} +0 -0
- /package/src/cli/options/{hummingbot.mjs → venues/hummingbot.mjs} +0 -0
- /package/src/cli/options/{polymarket.mjs → venues/polymarket.mjs} +0 -0
- /package/src/cli/options/{venue.mjs → venues/venue.mjs} +0 -0
- /package/src/core/{executionContext.mjs → execution/executionContext.mjs} +0 -0
- /package/src/core/{fundingTokenIn.mjs → execution/fundingTokenIn.mjs} +0 -0
- /package/src/core/{hostedRuntimeIdempotency.mjs → execution/hostedRuntimeIdempotency.mjs} +0 -0
- /package/src/core/{readOnlyActions.mjs → execution/readOnlyActions.mjs} +0 -0
- /package/src/core/{signedTx.mjs → execution/signedTx.mjs} +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { apiPost } from '../../../core/http.mjs';
|
|
2
|
+
import { withActionTimeout } from '../../../core/execution/actionTimeouts.mjs';
|
|
3
|
+
import { actionNameForApi } from '../../actions/selection.mjs';
|
|
4
|
+
import { isEmptyQueuedExecuteIntent } from '../managedReadExecuteRail.mjs';
|
|
5
|
+
import {
|
|
6
|
+
isReadOnlyAction,
|
|
7
|
+
shouldFallbackAgentWalletToExecuteIntent,
|
|
8
|
+
normalizeManagedExecuteResponse,
|
|
9
|
+
READ_FAST_PATH_PREFLIGHT,
|
|
10
|
+
fastPathWalletMismatch,
|
|
11
|
+
} from './managedResponse.mjs';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { normalizeIntentResponse } = require('../../../utils/cli/intentNormalization.cjs');
|
|
16
|
+
|
|
17
|
+
/** The agent_wallet lane: the API signs with the agent's execution wallet. */
|
|
18
|
+
export function createAgentWalletLane(deps, lanes) {
|
|
19
|
+
const {
|
|
20
|
+
buildManagedExecutePayload,
|
|
21
|
+
} = deps;
|
|
22
|
+
const { shouldUseManagedReadFastPath, runReadOnlyActionViaIntentRail, resolveManagedExecutionPreflight } = lanes;
|
|
23
|
+
|
|
24
|
+
async function executeManagedWalletRun(ctx, flags, resolved, options) {
|
|
25
|
+
// Market-data reads under agent_wallet must use /execute and keep the provider payload on the
|
|
26
|
+
// envelope (raw + parsed data.result). Routing them only through /execute-intent was the 0.2.34
|
|
27
|
+
// attempt to avoid a burial bug, but solo Hermes agent_wallet intents come back as an empty
|
|
28
|
+
// queued envelope with no universe. Live /execute still returns the Hyperliquid meta payload
|
|
29
|
+
// (often as a JSON string under data.result). Reads skip funding gates; routing still fails closed.
|
|
30
|
+
const isRead = isReadOnlyAction(resolved);
|
|
31
|
+
const skipPreflight = Boolean(options?.skipPreflight);
|
|
32
|
+
// A seeded preflight (Hyperliquid/Lighter/Veranta address defaults) already ran; reuse it.
|
|
33
|
+
const readFastPath =
|
|
34
|
+
!skipPreflight && !options?.managedPreflight && shouldUseManagedReadFastPath(flags, resolved, options);
|
|
35
|
+
let preflight = readFastPath ? READ_FAST_PATH_PREFLIGHT : null;
|
|
36
|
+
if (!skipPreflight && !readFastPath) {
|
|
37
|
+
preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
38
|
+
runMode: 'agent_wallet',
|
|
39
|
+
requireFundingCheck: !isRead,
|
|
40
|
+
preflightSeed: options?.managedPreflight,
|
|
41
|
+
});
|
|
42
|
+
if (!preflight.success) {
|
|
43
|
+
const normalized = normalizeIntentResponse({
|
|
44
|
+
success: false,
|
|
45
|
+
status: 'failed',
|
|
46
|
+
mode: 'agent_wallet',
|
|
47
|
+
blockedReason: preflight.blockedReason || 'PRECHECK_FAILED',
|
|
48
|
+
blockedStage: preflight.blockedStage || 'routing',
|
|
49
|
+
error: preflight.error || 'Managed wallet preflight failed',
|
|
50
|
+
preflightBlocked: true,
|
|
51
|
+
executionWalletAddress: preflight.executionWalletAddress || null,
|
|
52
|
+
executionWalletPreflight: preflight,
|
|
53
|
+
});
|
|
54
|
+
normalized.executionWalletAddress = preflight.executionWalletAddress || null;
|
|
55
|
+
normalized.executionWalletPreflight = preflight;
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const payload = buildManagedExecutePayload(flags, {
|
|
60
|
+
resolvedAction: resolved.resolvedAction,
|
|
61
|
+
apiAction: actionNameForApi(resolved),
|
|
62
|
+
runMode: 'agent_wallet',
|
|
63
|
+
forceIdempotency: true,
|
|
64
|
+
});
|
|
65
|
+
const actionCtx = withActionTimeout(ctx, resolved);
|
|
66
|
+
const response = await apiPost(actionCtx, '/api/v1/agent/execute', payload);
|
|
67
|
+
if (readFastPath) {
|
|
68
|
+
const mismatch = fastPathWalletMismatch(flags, response);
|
|
69
|
+
if (mismatch) return mismatch;
|
|
70
|
+
}
|
|
71
|
+
if (shouldFallbackAgentWalletToExecuteIntent(resolved, response)) {
|
|
72
|
+
// On the fast path the wallet comes from the /execute answer, not a preflight.
|
|
73
|
+
const fallbackPreflight = readFastPath
|
|
74
|
+
? {
|
|
75
|
+
...READ_FAST_PATH_PREFLIGHT,
|
|
76
|
+
executionWalletAddress: response?.executionWalletAddress || response?.data?.executionWalletAddress || null,
|
|
77
|
+
}
|
|
78
|
+
: preflight;
|
|
79
|
+
const intentResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, fallbackPreflight);
|
|
80
|
+
if (isEmptyQueuedExecuteIntent(intentResult.raw || intentResult)) {
|
|
81
|
+
// A read that did not answer in time (often a cold provider cache): nothing changed, and the
|
|
82
|
+
// same read usually answers on the next try. Same fields as thirdfy-mcp walletExecute.
|
|
83
|
+
intentResult.success = false;
|
|
84
|
+
intentResult.status = 'failed';
|
|
85
|
+
intentResult.error = intentResult.error || 'MANAGED_READ_EMPTY_EXECUTE_INTENT';
|
|
86
|
+
Object.assign(intentResult, {
|
|
87
|
+
errorCode: 'MANAGED_READ_EMPTY_EXECUTE_INTENT',
|
|
88
|
+
retryable: true,
|
|
89
|
+
retryAfterSeconds: 3,
|
|
90
|
+
nextAction: 'retry_later',
|
|
91
|
+
hint:
|
|
92
|
+
intentResult.hint ||
|
|
93
|
+
'The read did not answer in time; nothing was changed. Repeat the same read in a few seconds. Do not treat the empty queued result as data.',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
intentResult.executionWalletAddress =
|
|
97
|
+
preflight?.executionWalletAddress ||
|
|
98
|
+
response?.executionWalletAddress ||
|
|
99
|
+
response?.data?.executionWalletAddress ||
|
|
100
|
+
null;
|
|
101
|
+
return intentResult;
|
|
102
|
+
}
|
|
103
|
+
return normalizeManagedExecuteResponse(response, { preflight, payload, isRead });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
executeManagedWalletRun,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { ACTION_PREFLIGHT_VENUE_FIELDS, isApiActionBlock } from '../actionPreflight.mjs';
|
|
2
|
+
import { normalizeHybridWalletMode } from '../../../core/runMode.mjs';
|
|
3
|
+
import { apiPost } from '../../../core/http.mjs';
|
|
4
|
+
import { actionNameForApi } from '../../actions/selection.mjs';
|
|
5
|
+
import { isReadOnlyAction } from './managedResponse.mjs';
|
|
6
|
+
import { createFallbackHints } from './fallbackHints.mjs';
|
|
7
|
+
import { createManagedPreflight } from './managedPreflight.mjs';
|
|
8
|
+
import { createThirdfyLane } from './thirdfyLane.mjs';
|
|
9
|
+
import { createSelfLane } from './selfLane.mjs';
|
|
10
|
+
import { createAgentWalletLane } from './agentWalletLane.mjs';
|
|
11
|
+
import { createHybridLane } from './hybridLane.mjs';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { normalizeIntentResponse } = require('../../../utils/cli/intentNormalization.cjs');
|
|
16
|
+
|
|
17
|
+
/** Execution runners: the preflight and the run for each run mode, wired from the lane files. */
|
|
18
|
+
export function createExecutionRunners(deps) {
|
|
19
|
+
const {
|
|
20
|
+
buildIntentPayload,
|
|
21
|
+
shouldUseBuildTxPreflight,
|
|
22
|
+
} = deps;
|
|
23
|
+
const { applyExecutionFallbackHints } = createFallbackHints(deps);
|
|
24
|
+
const { shouldUseManagedReadFastPath, resolveManagedExecutionPreflight } = createManagedPreflight(deps);
|
|
25
|
+
const { executeThirdfyRun, runReadOnlyActionViaIntentRail } = createThirdfyLane(deps);
|
|
26
|
+
const { executeSelfRun, performSelfBroadcast, resolveEffectCheckResult, submitSignedTx } = createSelfLane(deps, { runReadOnlyActionViaIntentRail });
|
|
27
|
+
const { executeManagedWalletRun } = createAgentWalletLane(deps, { shouldUseManagedReadFastPath, resolveManagedExecutionPreflight, runReadOnlyActionViaIntentRail });
|
|
28
|
+
const { executeHybridRun } = createHybridLane(deps, { runReadOnlyActionViaIntentRail, executeManagedWalletRun, executeSelfRun });
|
|
29
|
+
|
|
30
|
+
async function runPreflightByMode(runMode, ctx, flags, resolved, options = {}) {
|
|
31
|
+
const resolvedAction = resolved.resolvedAction;
|
|
32
|
+
if (runMode === 'agent_wallet') {
|
|
33
|
+
const preflight = await resolveManagedExecutionPreflight(ctx, flags, resolved, {
|
|
34
|
+
runMode,
|
|
35
|
+
// Reads carry no transaction; keep funding optional so preflight matches run.
|
|
36
|
+
requireFundingCheck: !isReadOnlyAction(resolved),
|
|
37
|
+
preflightSeed: options.managedPreflight,
|
|
38
|
+
// Ask the API to check policy and venue readiness for this action, not just the wallet.
|
|
39
|
+
evaluateAction: true,
|
|
40
|
+
});
|
|
41
|
+
const normalized = normalizeIntentResponse({
|
|
42
|
+
success: preflight.success,
|
|
43
|
+
status: preflight.success ? 'ready' : 'failed',
|
|
44
|
+
mode: 'agent_wallet',
|
|
45
|
+
executionWalletAddress: preflight.executionWalletAddress || null,
|
|
46
|
+
signerMethod: preflight.signerMethod || 'managed_wallet_server',
|
|
47
|
+
blockedReason: preflight.blockedReason || null,
|
|
48
|
+
blockedStage: preflight.blockedStage || null,
|
|
49
|
+
error: preflight.success ? null : preflight.error || 'Managed wallet preflight failed',
|
|
50
|
+
preflightBlocked: !preflight.success,
|
|
51
|
+
fundingTokenBalance: preflight.fundingTokenBalance || null,
|
|
52
|
+
executionWalletPreflight: preflight,
|
|
53
|
+
});
|
|
54
|
+
normalized.executionWalletAddress = preflight.executionWalletAddress || null;
|
|
55
|
+
normalized.signerMethod = preflight.signerMethod || 'managed_wallet_server';
|
|
56
|
+
normalized.executionWalletPreflight = preflight;
|
|
57
|
+
normalized.fundingTokenBalance = preflight.fundingTokenBalance || null;
|
|
58
|
+
normalized.policyEvaluated = preflight.policyEvaluated === true;
|
|
59
|
+
for (const key of [...ACTION_PREFLIGHT_VENUE_FIELDS, 'executionVenue']) {
|
|
60
|
+
if (preflight[key] != null) normalized[key] = preflight[key];
|
|
61
|
+
}
|
|
62
|
+
// Only an API refusal of the action is `blocked`; CLI-local checks keep `failed`.
|
|
63
|
+
const apiBlocked = isApiActionBlock(preflight);
|
|
64
|
+
if (apiBlocked) normalized.status = 'blocked';
|
|
65
|
+
return {
|
|
66
|
+
message: preflight.success
|
|
67
|
+
? 'Managed wallet preflight completed'
|
|
68
|
+
: preflight.blockedStage === 'venue_readiness'
|
|
69
|
+
? 'Managed wallet preflight blocked: venue not ready'
|
|
70
|
+
: apiBlocked
|
|
71
|
+
? 'Managed wallet preflight blocked by policy'
|
|
72
|
+
: 'Managed wallet preflight failed',
|
|
73
|
+
route: 'execution_wallet_preflight',
|
|
74
|
+
normalized,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (runMode === 'self') {
|
|
78
|
+
// Reads have no unsigned tx to prepare. Keep preflight on validation-only execute-intent so
|
|
79
|
+
// operators dry-running self mode do not trigger a real catalog read (run still uses the read rail).
|
|
80
|
+
if (isReadOnlyAction(resolved)) {
|
|
81
|
+
const payload = buildIntentPayload(flags, {
|
|
82
|
+
validationOnly: true,
|
|
83
|
+
forceIdempotency: false,
|
|
84
|
+
resolvedAction,
|
|
85
|
+
apiAction: actionNameForApi(resolved),
|
|
86
|
+
runMode: 'self',
|
|
87
|
+
mirrorOnly: false,
|
|
88
|
+
});
|
|
89
|
+
const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
|
|
90
|
+
const normalized = normalizeIntentResponse(response);
|
|
91
|
+
const blockedCount = normalized.blocked || 0;
|
|
92
|
+
if (!normalized.success || blockedCount > 0) {
|
|
93
|
+
normalized.success = false;
|
|
94
|
+
normalized.error = normalized.error || 'Preflight blocked self execution';
|
|
95
|
+
normalized.preflightBlocked = true;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
message: normalized.success
|
|
99
|
+
? 'Self preflight completed'
|
|
100
|
+
: normalized.preflightBlocked
|
|
101
|
+
? 'Self preflight blocked by governance'
|
|
102
|
+
: 'Self preflight failed',
|
|
103
|
+
route: 'execute_intent_validation',
|
|
104
|
+
normalized,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const route = shouldUseBuildTxPreflight(runMode, resolved) ? 'build_tx' : 'execute_intent_validation';
|
|
108
|
+
const normalized = await executeSelfRun(
|
|
109
|
+
ctx,
|
|
110
|
+
flags,
|
|
111
|
+
resolved,
|
|
112
|
+
{ skipPreflight: route === 'build_tx' }
|
|
113
|
+
);
|
|
114
|
+
const blockedByGovernance = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
115
|
+
return {
|
|
116
|
+
message: blockedByGovernance
|
|
117
|
+
? 'Self preflight blocked by governance'
|
|
118
|
+
: normalized.success
|
|
119
|
+
? 'Self preflight completed (unsigned transaction ready)'
|
|
120
|
+
: 'Self preflight failed',
|
|
121
|
+
route,
|
|
122
|
+
normalized,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (runMode === 'hybrid') {
|
|
126
|
+
// Reads have nothing to mirror and need no wallet. Keep preflight on validation-only
|
|
127
|
+
// execute-intent so dry-runs do not trigger a real catalog read (run still uses the read rail).
|
|
128
|
+
if (isReadOnlyAction(resolved)) {
|
|
129
|
+
const payload = buildIntentPayload(flags, {
|
|
130
|
+
validationOnly: true,
|
|
131
|
+
forceIdempotency: false,
|
|
132
|
+
resolvedAction,
|
|
133
|
+
apiAction: actionNameForApi(resolved),
|
|
134
|
+
runMode: 'hybrid',
|
|
135
|
+
mirrorOnly: false,
|
|
136
|
+
hybridWalletMode: options.hybridWalletMode,
|
|
137
|
+
});
|
|
138
|
+
const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
|
|
139
|
+
const normalized = normalizeIntentResponse(response);
|
|
140
|
+
const blockedCount = normalized.blocked || 0;
|
|
141
|
+
if (!normalized.success || blockedCount > 0) {
|
|
142
|
+
normalized.success = false;
|
|
143
|
+
normalized.error = normalized.error || 'Preflight blocked hybrid execution';
|
|
144
|
+
normalized.preflightBlocked = true;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
message: normalized.success
|
|
148
|
+
? 'Hybrid preflight completed'
|
|
149
|
+
: normalized.preflightBlocked
|
|
150
|
+
? 'Hybrid preflight blocked by governance'
|
|
151
|
+
: 'Hybrid preflight failed',
|
|
152
|
+
route: 'execute_intent_validation',
|
|
153
|
+
normalized,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (options.hybridWalletMode === 'agent_wallet') {
|
|
157
|
+
const managedPayload = buildIntentPayload(flags, {
|
|
158
|
+
validationOnly: true,
|
|
159
|
+
forceIdempotency: false,
|
|
160
|
+
resolvedAction,
|
|
161
|
+
apiAction: actionNameForApi(resolved),
|
|
162
|
+
runMode: 'agent_wallet',
|
|
163
|
+
mirrorOnly: false,
|
|
164
|
+
hybridWalletMode: 'agent_wallet',
|
|
165
|
+
});
|
|
166
|
+
const managedResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', managedPayload);
|
|
167
|
+
const managedNormalized = normalizeIntentResponse(managedResponse);
|
|
168
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
169
|
+
validationOnly: true,
|
|
170
|
+
forceIdempotency: false,
|
|
171
|
+
resolvedAction,
|
|
172
|
+
apiAction: actionNameForApi(resolved),
|
|
173
|
+
runMode: 'hybrid',
|
|
174
|
+
mirrorOnly: true,
|
|
175
|
+
hybridWalletMode: 'agent_wallet',
|
|
176
|
+
});
|
|
177
|
+
const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
178
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
179
|
+
return {
|
|
180
|
+
message:
|
|
181
|
+
managedNormalized.success && mirrorNormalized.success
|
|
182
|
+
? 'Hybrid preflight completed (agent_wallet + thirdfy mirror)'
|
|
183
|
+
: 'Hybrid preflight failed (agent_wallet + thirdfy mirror)',
|
|
184
|
+
route: 'execute_intent_validation_dual',
|
|
185
|
+
normalized: normalizeIntentResponse({
|
|
186
|
+
success: Boolean(managedNormalized.success && mirrorNormalized.success),
|
|
187
|
+
status: mirrorNormalized.status || managedNormalized.status || 'failed',
|
|
188
|
+
mode: 'hybrid',
|
|
189
|
+
hybridWalletMode: 'agent_wallet',
|
|
190
|
+
managed: managedNormalized,
|
|
191
|
+
mirror: mirrorNormalized,
|
|
192
|
+
error: managedNormalized.error || mirrorNormalized.error || null,
|
|
193
|
+
}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const payload = buildIntentPayload(flags, {
|
|
198
|
+
validationOnly: true,
|
|
199
|
+
forceIdempotency: false,
|
|
200
|
+
resolvedAction,
|
|
201
|
+
apiAction: actionNameForApi(resolved),
|
|
202
|
+
runMode,
|
|
203
|
+
mirrorOnly: runMode === 'hybrid',
|
|
204
|
+
});
|
|
205
|
+
const response = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', payload);
|
|
206
|
+
const normalized = normalizeIntentResponse(response);
|
|
207
|
+
return {
|
|
208
|
+
message: normalized.success
|
|
209
|
+
? runMode === 'hybrid'
|
|
210
|
+
? 'Hybrid mirror preflight completed'
|
|
211
|
+
: 'Preflight completed'
|
|
212
|
+
: runMode === 'hybrid'
|
|
213
|
+
? 'Hybrid mirror preflight failed'
|
|
214
|
+
: 'Preflight failed',
|
|
215
|
+
route: 'execute_intent_validation',
|
|
216
|
+
normalized,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function runExecutionByMode(runMode, ctx, flags, resolved, options) {
|
|
221
|
+
if (runMode === 'thirdfy') {
|
|
222
|
+
const normalized = await executeThirdfyRun(ctx, flags, resolved, options, runMode);
|
|
223
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
224
|
+
return {
|
|
225
|
+
code: normalized.success ? 'RUN_QUEUED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
|
|
226
|
+
message: normalized.success
|
|
227
|
+
? 'Execution intent queued'
|
|
228
|
+
: preflightBlocked
|
|
229
|
+
? 'Preflight blocked execution'
|
|
230
|
+
: 'Execution failed',
|
|
231
|
+
idempotencyKey: normalized.idempotencyKey || null,
|
|
232
|
+
normalized,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (runMode === 'agent_wallet') {
|
|
236
|
+
const normalized = await executeManagedWalletRun(ctx, flags, resolved, options);
|
|
237
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
238
|
+
return {
|
|
239
|
+
code: normalized.success ? 'WALLET_EXECUTED' : preflightBlocked ? 'PREFLIGHT_BLOCKED' : 'RUN_FAILED',
|
|
240
|
+
message: normalized.success
|
|
241
|
+
? 'Managed wallet execution completed'
|
|
242
|
+
: preflightBlocked
|
|
243
|
+
? 'Managed wallet preflight blocked execution'
|
|
244
|
+
: 'Managed wallet execution failed',
|
|
245
|
+
idempotencyKey: normalized.idempotencyKey || null,
|
|
246
|
+
normalized,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (runMode === 'self') {
|
|
250
|
+
const normalized = await executeSelfRun(ctx, flags, resolved, options);
|
|
251
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
252
|
+
const readOnlySuccess =
|
|
253
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
254
|
+
return {
|
|
255
|
+
code: normalized.success
|
|
256
|
+
? readOnlySuccess
|
|
257
|
+
? 'SELF_READ_COMPLETED'
|
|
258
|
+
: 'SELF_UNSIGNED_TX_READY'
|
|
259
|
+
: preflightBlocked
|
|
260
|
+
? 'PREFLIGHT_BLOCKED'
|
|
261
|
+
: 'SELF_EXECUTION_FAILED',
|
|
262
|
+
message: normalized.success
|
|
263
|
+
? readOnlySuccess
|
|
264
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
265
|
+
: 'Unsigned transaction prepared for self-custody execution'
|
|
266
|
+
: preflightBlocked
|
|
267
|
+
? 'Preflight blocked self execution'
|
|
268
|
+
: 'Self-custody execution failed',
|
|
269
|
+
idempotencyKey: null,
|
|
270
|
+
normalized,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const normalized = await executeHybridRun(ctx, flags, resolved, options);
|
|
274
|
+
const preflightBlocked = !normalized.success && Boolean(normalized.preflightBlocked);
|
|
275
|
+
const hybridWalletMode = normalizeHybridWalletMode(
|
|
276
|
+
normalized.hybridWalletMode || options?.hybridWalletMode || flags.hybridWalletMode || 'self',
|
|
277
|
+
);
|
|
278
|
+
const readOnlySuccess =
|
|
279
|
+
normalized.success && normalized.routeFallback === 'execute_intent_read_only';
|
|
280
|
+
return {
|
|
281
|
+
code: normalized.success
|
|
282
|
+
? readOnlySuccess
|
|
283
|
+
? 'HYBRID_READ_COMPLETED'
|
|
284
|
+
: 'HYBRID_READY'
|
|
285
|
+
: preflightBlocked
|
|
286
|
+
? 'PREFLIGHT_BLOCKED'
|
|
287
|
+
: 'HYBRID_FAILED',
|
|
288
|
+
message: normalized.success
|
|
289
|
+
? readOnlySuccess
|
|
290
|
+
? 'Read-only action completed via delegated execute-intent'
|
|
291
|
+
: hybridWalletMode === 'agent_wallet'
|
|
292
|
+
? 'Hybrid execution prepared (agent wallet + mirror preflight)'
|
|
293
|
+
: 'Hybrid execution prepared (self tx + mirror preflight)'
|
|
294
|
+
: preflightBlocked
|
|
295
|
+
? 'Preflight blocked hybrid execution'
|
|
296
|
+
: 'Hybrid execution failed',
|
|
297
|
+
idempotencyKey: normalized.idempotencyKey || null,
|
|
298
|
+
normalized,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
applyExecutionFallbackHints,
|
|
304
|
+
runPreflightByMode,
|
|
305
|
+
runExecutionByMode,
|
|
306
|
+
executeThirdfyRun,
|
|
307
|
+
executeManagedWalletRun,
|
|
308
|
+
executeSelfRun,
|
|
309
|
+
executeHybridRun,
|
|
310
|
+
resolveManagedExecutionPreflight,
|
|
311
|
+
performSelfBroadcast,
|
|
312
|
+
resolveEffectCheckResult,
|
|
313
|
+
submitSignedTx,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Hints added to a failed execution answer (Bitfinex onboarding and similar next steps). */
|
|
2
|
+
export function createFallbackHints(deps) {
|
|
3
|
+
const {
|
|
4
|
+
buildBitfinexOnboardingCommand,
|
|
5
|
+
inferBitfinexPermissionProfileFromAction,
|
|
6
|
+
} = deps;
|
|
7
|
+
|
|
8
|
+
function applyExecutionFallbackHints(normalized, { flags, runMode, resolvedAction }) {
|
|
9
|
+
if (!normalized || typeof normalized !== 'object') return normalized;
|
|
10
|
+
const next = { ...normalized };
|
|
11
|
+
const reasons = new Set();
|
|
12
|
+
const topBlockedReason = String(next.blockedReason || '').trim();
|
|
13
|
+
if (topBlockedReason) reasons.add(topBlockedReason);
|
|
14
|
+
const blockedByReason = next.blockedByReason && typeof next.blockedByReason === 'object' ? next.blockedByReason : {};
|
|
15
|
+
for (const key of Object.keys(blockedByReason)) {
|
|
16
|
+
if (String(key || '').trim()) reasons.add(String(key).trim());
|
|
17
|
+
}
|
|
18
|
+
const resolvedActionLower = String(resolvedAction || '').trim().toLowerCase();
|
|
19
|
+
const looksBitfinexAction = resolvedActionLower.includes('bitfinex');
|
|
20
|
+
const permissionProfile = inferBitfinexPermissionProfileFromAction(resolvedActionLower);
|
|
21
|
+
const cexCredentialMissingSignals = new Set([
|
|
22
|
+
'CEX_CREDENTIAL_NOT_FOUND',
|
|
23
|
+
'BITFINEX_CREDENTIALS_NOT_IN_CONTEXT',
|
|
24
|
+
'BITFINEX_CREDENTIALS_NOT_IN_THIRDFY_DELEGATION_CONTEXT',
|
|
25
|
+
'BITFINEX_ENV_CREDENTIALS_NOT_CONFIGURED',
|
|
26
|
+
]);
|
|
27
|
+
const hasMissingCredentialSignal = Array.from(reasons).some((code) => cexCredentialMissingSignals.has(code));
|
|
28
|
+
if (looksBitfinexAction && hasMissingCredentialSignal) {
|
|
29
|
+
next.onboardingHint = {
|
|
30
|
+
type: 'bitfinex_credentials',
|
|
31
|
+
message:
|
|
32
|
+
'Bitfinex credentials are missing for this delegated account. Onboard credentials first, then retry the same command.',
|
|
33
|
+
command: buildBitfinexOnboardingCommand(permissionProfile),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
looksBitfinexAction &&
|
|
38
|
+
runMode !== 'thirdfy' &&
|
|
39
|
+
runMode !== 'agent_wallet' &&
|
|
40
|
+
(topBlockedReason === 'SELF_MODE_REQUIRES_BUILD_TX' || topBlockedReason === 'HYBRID_MODE_REQUIRES_BUILD_TX')
|
|
41
|
+
) {
|
|
42
|
+
next.onboardingHint = {
|
|
43
|
+
...(next.onboardingHint || {}),
|
|
44
|
+
runMode: 'thirdfy',
|
|
45
|
+
runModeMessage: 'Bitfinex actions require delegated execute-intent rail. Re-run with --run-mode thirdfy.',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
applyExecutionFallbackHints,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { normalizeHybridWalletMode } from '../../../core/runMode.mjs';
|
|
2
|
+
import { apiPost } from '../../../core/http.mjs';
|
|
3
|
+
import { actionNameForApi } from '../../actions/selection.mjs';
|
|
4
|
+
import { isReadOnlyAction } from './managedResponse.mjs';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { normalizeIntentResponse } = require('../../../utils/cli/intentNormalization.cjs');
|
|
9
|
+
|
|
10
|
+
/** The hybrid lane: the managed wallet or self signing, per the hybrid wallet mode. */
|
|
11
|
+
export function createHybridLane(deps, lanes) {
|
|
12
|
+
const {
|
|
13
|
+
buildIntentPayload,
|
|
14
|
+
} = deps;
|
|
15
|
+
const { runReadOnlyActionViaIntentRail, executeManagedWalletRun, executeSelfRun } = lanes;
|
|
16
|
+
|
|
17
|
+
async function executeHybridRun(ctx, flags, resolved, options) {
|
|
18
|
+
const hybridWalletMode = normalizeHybridWalletMode(options?.hybridWalletMode || flags.hybridWalletMode || 'self');
|
|
19
|
+
// Reads have nothing to mirror and need no wallet, whichever hybrid wallet mode is selected.
|
|
20
|
+
// Answering from the read rail also keeps the payload at the top level, instead of nesting it under
|
|
21
|
+
// `managed` where callers that read `results` cannot find it.
|
|
22
|
+
if (isReadOnlyAction(resolved)) {
|
|
23
|
+
const readResult = await runReadOnlyActionViaIntentRail(ctx, flags, resolved, options, null, 'hybrid');
|
|
24
|
+
readResult.hybridWalletMode = hybridWalletMode;
|
|
25
|
+
return readResult;
|
|
26
|
+
}
|
|
27
|
+
if (hybridWalletMode === 'agent_wallet') {
|
|
28
|
+
const managedResult = await executeManagedWalletRun(ctx, { ...flags, runMode: 'agent_wallet' }, resolved, options);
|
|
29
|
+
if (!managedResult.success) {
|
|
30
|
+
return normalizeIntentResponse({
|
|
31
|
+
...managedResult,
|
|
32
|
+
mode: 'hybrid',
|
|
33
|
+
hybridWalletMode,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
37
|
+
validationOnly: true,
|
|
38
|
+
forceIdempotency: true,
|
|
39
|
+
resolvedAction: resolved.resolvedAction,
|
|
40
|
+
apiAction: actionNameForApi(resolved),
|
|
41
|
+
runMode: 'hybrid',
|
|
42
|
+
mirrorOnly: true,
|
|
43
|
+
hybridWalletMode,
|
|
44
|
+
});
|
|
45
|
+
const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
46
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
47
|
+
return normalizeIntentResponse({
|
|
48
|
+
success: Boolean(managedResult.success && mirrorNormalized.success),
|
|
49
|
+
status: managedResult.status || mirrorNormalized.status || 'queued',
|
|
50
|
+
mode: 'hybrid',
|
|
51
|
+
hybridWalletMode,
|
|
52
|
+
idempotencyKey: managedResult.idempotencyKey || mirrorPayload.idempotencyKey,
|
|
53
|
+
managed: managedResult,
|
|
54
|
+
mirror: mirrorNormalized,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const selfResult = await executeSelfRun(ctx, flags, resolved, {
|
|
58
|
+
...(options || {}),
|
|
59
|
+
runMode: 'hybrid',
|
|
60
|
+
blockedReasonCode: 'HYBRID_MODE_REQUIRES_BUILD_TX',
|
|
61
|
+
});
|
|
62
|
+
if (!selfResult.success) return selfResult;
|
|
63
|
+
|
|
64
|
+
const mirrorPayload = buildIntentPayload(flags, {
|
|
65
|
+
validationOnly: true,
|
|
66
|
+
forceIdempotency: true,
|
|
67
|
+
resolvedAction: resolved.resolvedAction,
|
|
68
|
+
apiAction: actionNameForApi(resolved),
|
|
69
|
+
runMode: 'hybrid',
|
|
70
|
+
mirrorOnly: true,
|
|
71
|
+
});
|
|
72
|
+
const mirrorResponse = await apiPost({ ...ctx, writeAction: false }, '/api/v1/agent/execute-intent', mirrorPayload);
|
|
73
|
+
const mirrorNormalized = normalizeIntentResponse(mirrorResponse);
|
|
74
|
+
return normalizeIntentResponse({
|
|
75
|
+
success: Boolean(selfResult.success && mirrorNormalized.success),
|
|
76
|
+
status: mirrorNormalized.status || 'ready',
|
|
77
|
+
mode: 'hybrid',
|
|
78
|
+
hybridWalletMode,
|
|
79
|
+
idempotencyKey: mirrorPayload.idempotencyKey,
|
|
80
|
+
self: selfResult,
|
|
81
|
+
mirror: mirrorNormalized,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
executeHybridRun,
|
|
87
|
+
};
|
|
88
|
+
}
|