@reefclaw/openclaw-plugin 0.1.22 → 0.1.23

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/bridge/bridge.js CHANGED
@@ -505,6 +505,44 @@ export class Bridge {
505
505
  }
506
506
  return;
507
507
  }
508
+ if (method === 'hl_submit_agent_approval') {
509
+ const provider = this.provider;
510
+ if (!provider.submitHlAgentApproval) {
511
+ this.connector.sendResponse(id, true, {
512
+ ok: false,
513
+ message: 'Provider does not support hl_submit_agent_approval (likely mock provider)',
514
+ });
515
+ return;
516
+ }
517
+ const action = params?.action;
518
+ const nonce = params?.nonce;
519
+ const signature = params?.signature;
520
+ if (!action || typeof action !== 'object' ||
521
+ typeof nonce !== 'number' ||
522
+ !signature || typeof signature.r !== 'string' || typeof signature.s !== 'string' ||
523
+ typeof signature.v !== 'number') {
524
+ this.connector.sendResponse(id, false, undefined, {
525
+ code: 400,
526
+ message: 'hl_submit_agent_approval requires { action, nonce, signature:{r,s,v} }',
527
+ });
528
+ return;
529
+ }
530
+ // The plugin re-validates strictly (approveAgent only, own agent
531
+ // address only) — this layer just shape-checks and forwards.
532
+ audit('hl_submit_agent_approval.start', {
533
+ id,
534
+ chain: action.hyperliquidChain,
535
+ agentAddress: action.agentAddress,
536
+ });
537
+ const outcome = await provider.submitHlAgentApproval({
538
+ action: action,
539
+ nonce,
540
+ signature: { r: signature.r, s: signature.s, v: signature.v },
541
+ });
542
+ audit('hl_submit_agent_approval.complete', { id, ok: outcome.ok, hlStatus: outcome.hlStatus });
543
+ this.connector.sendResponse(id, true, outcome);
544
+ return;
545
+ }
508
546
  if (method === 'hl_agent_wallet_status') {
509
547
  const provider = this.provider;
510
548
  if (!provider.getHlAgentWalletStatus) {
@@ -3,7 +3,7 @@ import { GatewayHttpClient, GatewayHttpError } from './gateway-http-client.js';
3
3
  * Logical tool names used by ReefClaw internally.
4
4
  * Each maps to one or more candidate actual tool names on the gateway.
5
5
  */
6
- export type LogicalTool = 'fetch_ticker' | 'fetch_balance' | 'fetch_ohlcv' | 'fetch_positions' | 'fetch_open_orders' | 'cancel_all_orders' | 'cancel_order' | 'close_position' | 'create_order' | 'get_market_structure' | 'get_crypto_metrics' | 'get_market_intel' | 'set_trading_mode' | 'set_exchange_credentials' | 'test_exchange_credentials' | 'clear_exchange_credentials' | 'hl_provision_agent_wallet' | 'hl_agent_wallet_status' | 'get_bracket_config' | 'set_bracket_requirement';
6
+ export type LogicalTool = 'fetch_ticker' | 'fetch_balance' | 'fetch_ohlcv' | 'fetch_positions' | 'fetch_open_orders' | 'cancel_all_orders' | 'cancel_order' | 'close_position' | 'create_order' | 'get_market_structure' | 'get_crypto_metrics' | 'get_market_intel' | 'set_trading_mode' | 'set_exchange_credentials' | 'test_exchange_credentials' | 'clear_exchange_credentials' | 'hl_provision_agent_wallet' | 'hl_agent_wallet_status' | 'hl_submit_agent_approval' | 'get_bracket_config' | 'set_bracket_requirement';
7
7
  /**
8
8
  * Build the test args used when probing each logical tool.
9
9
  *
@@ -22,6 +22,7 @@ const OPTIONAL_TOOLS = [
22
22
  'clear_exchange_credentials',
23
23
  'hl_provision_agent_wallet',
24
24
  'hl_agent_wallet_status',
25
+ 'hl_submit_agent_approval',
25
26
  'get_bracket_config',
26
27
  'set_bracket_requirement',
27
28
  ];
@@ -48,6 +49,7 @@ const CANDIDATES = {
48
49
  clear_exchange_credentials: ['clear_exchange_credentials'],
49
50
  hl_provision_agent_wallet: ['hl_provision_agent_wallet'],
50
51
  hl_agent_wallet_status: ['hl_agent_wallet_status'],
52
+ hl_submit_agent_approval: ['hl_submit_agent_approval'],
51
53
  get_bracket_config: ['get_bracket_config'],
52
54
  set_bracket_requirement: ['set_bracket_requirement'],
53
55
  };
@@ -119,6 +121,8 @@ export function buildProbeArgs(symbol = DEFAULT_PROBE_SYMBOL) {
119
121
  // timeout and mark the tool missing forever).
120
122
  hl_provision_agent_wallet: { walletAddress: '' },
121
123
  hl_agent_wallet_status: { probe: true },
124
+ // Same dedicated probe path: a real submit would POST to Hyperliquid.
125
+ hl_submit_agent_approval: { probe: true },
122
126
  // Bracket-config probes: no-arg read; set with an invalid flag triggers early-return.
123
127
  get_bracket_config: {},
124
128
  set_bracket_requirement: { flag: '__probe__', value: false },
@@ -39,6 +39,15 @@ export interface HlProvisionOutcome {
39
39
  currentVenue?: string;
40
40
  currentMode?: string;
41
41
  }
42
+ /** Result of hl_submit_agent_approval — the box relays the wallet-signed
43
+ * approveAgent action to Hyperliquid (browsers are often blocked). */
44
+ export interface HlSubmitApprovalOutcome {
45
+ ok: boolean;
46
+ message: string;
47
+ hlStatus?: string;
48
+ chain?: string;
49
+ agentAddress?: string;
50
+ }
42
51
  /** Result of hl_agent_wallet_status — approval polling for the guided flow. */
43
52
  export interface HlAgentWalletStatusOutcome {
44
53
  ok: boolean;
@@ -196,6 +205,18 @@ export interface OpenClawProvider {
196
205
  /** Operator-only: approval/balance status of the box's provisioned HL agent
197
206
  * wallet (polled by the dashboard's guided flow after the wallet signature). */
198
207
  getHlAgentWalletStatus?(): Promise<HlAgentWalletStatusOutcome>;
208
+ /** Operator-only: submit the wallet-signed approveAgent action to
209
+ * Hyperliquid FROM THE BOX. Carries no secret — only a signature the
210
+ * operator's wallet already produced. */
211
+ submitHlAgentApproval?(args: {
212
+ action: Record<string, unknown>;
213
+ nonce: number;
214
+ signature: {
215
+ r: string;
216
+ s: string;
217
+ v: number;
218
+ };
219
+ }): Promise<HlSubmitApprovalOutcome>;
199
220
  /** Operator-only: remove stored credentials from the plugin config and
200
221
  * de-escalate to PAPER mode if currently running in a live mode. */
201
222
  clearExchangeCredentials?(): Promise<{
@@ -1,4 +1,4 @@
1
- import type { OpenClawProvider, ProviderEvents, ProviderEventName, ExchangeCredentialsRequest, HlProvisionOutcome, HlAgentWalletStatusOutcome } from '../provider.js';
1
+ import type { OpenClawProvider, ProviderEvents, ProviderEventName, ExchangeCredentialsRequest, HlProvisionOutcome, HlAgentWalletStatusOutcome, HlSubmitApprovalOutcome } from '../provider.js';
2
2
  import type { EmergencyAction, ReconciliationSnapshot, TradingMode } from '../types.js';
3
3
  import type { GatewayConfig } from '../gateway/gateway-config.js';
4
4
  import { type GetBracketConfigOutcome, type SetBracketRequirementOutcome, type BracketRequirementFlag, type SetTradingModeOutcome, type SetExchangeCredentialsOutcome, type TestExchangeCredentialsOutcome, type ClearExchangeCredentialsOutcome } from './onboarding-commands.js';
@@ -229,6 +229,16 @@ export declare class GatewayProvider implements OpenClawProvider {
229
229
  regenerate?: boolean;
230
230
  confirm_venue_switch?: boolean;
231
231
  }): Promise<HlProvisionOutcome>;
232
+ /** Operator-only. Relay the wallet-signed approveAgent action from the box. */
233
+ submitHlAgentApproval(args: {
234
+ action: Record<string, unknown>;
235
+ nonce: number;
236
+ signature: {
237
+ r: string;
238
+ s: string;
239
+ v: number;
240
+ };
241
+ }): Promise<HlSubmitApprovalOutcome>;
232
242
  /** Operator-only. Approval/balance status of the provisioned HL wallet. */
233
243
  getHlAgentWalletStatus(): Promise<HlAgentWalletStatusOutcome>;
234
244
  /** Operator-only. Read the current bracket-orders config from the plugin. */
@@ -17,7 +17,7 @@ import { ensureHeartbeatCron, isHeartbeatLikeName } from '../gateway/heartbeat-c
17
17
  import { parseIdentityName } from '../utils/identity-name.js';
18
18
  import { computeEquity as _computeEquity, computePositionNotional as _computePositionNotional, computeRiskMetrics as _computeRiskMetrics, DEFAULT_RISK_LIMITS, DRAWDOWN_ZONE_THRESHOLDS, TENANT_LIMIT_BOUNDS, boundedNum, validateDrawdownLadder, } from './risk-calculator.js';
19
19
  import { executeKill as _executeKill, executeFlatten as _executeFlatten, executePause as _executePause, executeResume as _executeResume, } from './emergency-commands.js';
20
- import { executeSetTradingMode, executeGetBracketConfig, executeSetBracketRequirement, executeSetExchangeCredentials, executeTestExchangeCredentials, executeClearExchangeCredentials, executeProvisionHlAgentWallet, executeHlAgentWalletStatus, } from './onboarding-commands.js';
20
+ import { executeSetTradingMode, executeGetBracketConfig, executeSetBracketRequirement, executeSetExchangeCredentials, executeTestExchangeCredentials, executeClearExchangeCredentials, executeProvisionHlAgentWallet, executeHlAgentWalletStatus, executeSubmitHlAgentApproval, } from './onboarding-commands.js';
21
21
  const TAG = 'gateway';
22
22
  // ---- Day-start NAV persistence ----
23
23
  // Persists sessionStartNav (the UTC-day P&L anchor) per date so Day P&L
@@ -705,6 +705,10 @@ export class GatewayProvider {
705
705
  async provisionHlAgentWallet(args) {
706
706
  return executeProvisionHlAgentWallet({ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, args);
707
707
  }
708
+ /** Operator-only. Relay the wallet-signed approveAgent action from the box. */
709
+ async submitHlAgentApproval(args) {
710
+ return executeSubmitHlAgentApproval({ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, args);
711
+ }
708
712
  /** Operator-only. Approval/balance status of the provisioned HL wallet. */
709
713
  async getHlAgentWalletStatus() {
710
714
  return executeHlAgentWalletStatus({
@@ -1,6 +1,6 @@
1
1
  import type { GatewayHttpClient } from '../gateway/gateway-http-client.js';
2
2
  import type { ToolMap } from '../gateway/tool-discovery.js';
3
- import type { ExchangeCredentialsRequest, HlProvisionOutcome, HlAgentWalletStatusOutcome } from '../provider.js';
3
+ import type { ExchangeCredentialsRequest, HlProvisionOutcome, HlAgentWalletStatusOutcome, HlSubmitApprovalOutcome } from '../provider.js';
4
4
  import type { TradingMode } from '@reefclaw/shared';
5
5
  export interface SetTradingModeOutcome {
6
6
  ok: boolean;
@@ -116,6 +116,17 @@ export declare function executeProvisionHlAgentWallet(ctx: OnboardingContext, ar
116
116
  regenerate?: boolean;
117
117
  confirm_venue_switch?: boolean;
118
118
  }): Promise<HlProvisionOutcome>;
119
+ /** Forward an hl_submit_agent_approval invocation to the plugin. The payload
120
+ * is a signature the operator's wallet produced — no secret material. */
121
+ export declare function executeSubmitHlAgentApproval(ctx: OnboardingContext, args: {
122
+ action: Record<string, unknown>;
123
+ nonce: number;
124
+ signature: {
125
+ r: string;
126
+ s: string;
127
+ v: number;
128
+ };
129
+ }): Promise<HlSubmitApprovalOutcome>;
119
130
  /** Forward an hl_agent_wallet_status invocation to the plugin. Read-only. */
120
131
  export declare function executeHlAgentWalletStatus(ctx: OnboardingContext): Promise<HlAgentWalletStatusOutcome>;
121
132
  /** Forward a set_exchange_credentials invocation to the plugin via HTTP.
@@ -254,6 +254,31 @@ export async function executeProvisionHlAgentWallet(ctx, args) {
254
254
  return { ok: false, message: reason, venue: 'hyperliquid', mode: 'PAPER' };
255
255
  }
256
256
  }
257
+ /** Forward an hl_submit_agent_approval invocation to the plugin. The payload
258
+ * is a signature the operator's wallet produced — no secret material. */
259
+ export async function executeSubmitHlAgentApproval(ctx, args) {
260
+ const tool = ctx.toolMap.hl_submit_agent_approval;
261
+ if (!tool || !ctx.http) {
262
+ return {
263
+ ok: false,
264
+ message: 'hl_submit_agent_approval tool not available on gateway — update the ReefClaw plugin (npx @reefclaw/connect@latest)',
265
+ };
266
+ }
267
+ try {
268
+ const result = await ctx.http.invoke(tool, {
269
+ action: args.action,
270
+ nonce: args.nonce,
271
+ signature: args.signature,
272
+ operator_token: ctx.operatorToken,
273
+ });
274
+ return result.data;
275
+ }
276
+ catch (err) {
277
+ const reason = err instanceof Error ? err.message : String(err);
278
+ logger.error(TAG, `hl_submit_agent_approval failed: ${reason}`);
279
+ return { ok: false, message: reason };
280
+ }
281
+ }
257
282
  /** Forward an hl_agent_wallet_status invocation to the plugin. Read-only. */
258
283
  export async function executeHlAgentWalletStatus(ctx) {
259
284
  const tool = ctx.toolMap.hl_agent_wallet_status;
package/bridge/types.d.ts CHANGED
@@ -3,7 +3,7 @@ export type { Channel, RequestFrame, ResponseFrame, EventFrame, Frame, Emergency
3
3
  export { VALID_CHANNELS, VALID_EMERGENCY_ACTIONS } from '@reefclaw/shared';
4
4
  type EventFrame = _EventFrame;
5
5
  /** Methods the skill accepts from the relay (browser → skill) */
6
- export declare const ALLOWED_METHODS: readonly ["emergency.kill", "emergency.flatten", "emergency.pause", "emergency.resume", "reconcile", "chat.send", "skill.update", "close_position", "set_trading_mode", "set_exchange_credentials", "test_exchange_credentials", "clear_exchange_credentials", "hl_provision_agent_wallet", "hl_agent_wallet_status", "get_bracket_config", "set_bracket_requirement"];
6
+ export declare const ALLOWED_METHODS: readonly ["emergency.kill", "emergency.flatten", "emergency.pause", "emergency.resume", "reconcile", "chat.send", "skill.update", "close_position", "set_trading_mode", "set_exchange_credentials", "test_exchange_credentials", "clear_exchange_credentials", "hl_provision_agent_wallet", "hl_agent_wallet_status", "hl_submit_agent_approval", "get_bracket_config", "set_bracket_requirement"];
7
7
  /** Subset of ALLOWED_METHODS that require operator.write scope. The bridge
8
8
  * enforces this before dispatching — a session without the scope gets a
9
9
  * 403 error. PR2 ships the scope as session-wide (inherited from the Clerk
package/bridge/types.js CHANGED
@@ -21,6 +21,7 @@ export const ALLOWED_METHODS = [
21
21
  // Hyperliquid guided onboarding (box-generated agent wallet) — operator.write-gated.
22
22
  'hl_provision_agent_wallet',
23
23
  'hl_agent_wallet_status',
24
+ 'hl_submit_agent_approval',
24
25
  // Bracket-orders config (Phase 3.5b) — operator.write-gated.
25
26
  'get_bracket_config',
26
27
  'set_bracket_requirement',
@@ -38,6 +39,7 @@ export const OPERATOR_WRITE_METHODS = new Set([
38
39
  'clear_exchange_credentials',
39
40
  'hl_provision_agent_wallet',
40
41
  'hl_agent_wallet_status',
42
+ 'hl_submit_agent_approval',
41
43
  'get_bracket_config',
42
44
  'set_bracket_requirement',
43
45
  'emergency.kill',
@@ -35,6 +35,7 @@ export const UNGOVERNABLE_TOOLS = new Set([
35
35
  // operator-only channel; disabling them would strand the HL setup flow.
36
36
  'hl_provision_agent_wallet',
37
37
  'hl_agent_wallet_status',
38
+ 'hl_submit_agent_approval',
38
39
  ]);
39
40
  /** RC_TOOL_GATE: 'off' = kill-switch (gate never blocks), 'shadow' = log
40
41
  * would-be blocks but execute anyway, anything else/default = 'enforce'.
package/index.js CHANGED
@@ -68,6 +68,7 @@ import { testExchangeCredentialsTool } from './tools/test-exchange-credentials.j
68
68
  import { clearExchangeCredentialsTool } from './tools/clear-exchange-credentials.js';
69
69
  import { hlProvisionAgentWalletTool } from './tools/hl-provision-agent-wallet.js';
70
70
  import { hlAgentWalletStatusTool } from './tools/hl-agent-wallet-status.js';
71
+ import { hlSubmitAgentApprovalTool } from './tools/hl-submit-agent-approval.js';
71
72
  import { ensureCredentialTransportKey } from './security/sealed-credentials.js';
72
73
  // Tool implementations
73
74
  import { fetchTickerTool } from './tools/fetch-ticker.js';
@@ -850,6 +851,16 @@ const TOOL_PARAMS = {
850
851
  probe: { type: 'boolean', description: 'Tool-discovery probe — returns immediately without config or network access' },
851
852
  },
852
853
  },
854
+ hl_submit_agent_approval: {
855
+ type: 'object',
856
+ properties: {
857
+ operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
858
+ action: { type: 'object', description: "The wallet-signed approveAgent action, verbatim. Only type='approveAgent' is accepted." },
859
+ nonce: { type: 'number', description: 'Outer nonce — must equal action.nonce (user-signed actions carry one nonce)' },
860
+ signature: { type: 'object', description: 'Wallet signature { r, s, v } over the approveAgent typed data' },
861
+ probe: { type: 'boolean', description: 'Tool-discovery probe — returns immediately without config or network access' },
862
+ },
863
+ },
853
864
  clear_exchange_credentials: {
854
865
  type: 'object',
855
866
  properties: {
@@ -2726,6 +2737,20 @@ const paperTradingPlugin = {
2726
2737
  return jsonResult(await hlProvisionAgentWalletTool(params, { runtime, bootVenue: venue }));
2727
2738
  },
2728
2739
  },
2740
+ {
2741
+ name: 'hl_submit_agent_approval',
2742
+ label: 'Submit Hyperliquid Agent Approval',
2743
+ description: "Operator-only. Submit the operator's wallet-signed approveAgent action to Hyperliquid FROM THIS MACHINE (the dashboard browser is often blocked from reaching the exchange API by ad-blockers/shields). Strictly limited to type='approveAgent' targeting this box's own provisioned agent wallet — it is not a general signed-action relay. Refused without dashboard operator provenance.",
2744
+ parameters: TOOL_PARAMS.hl_submit_agent_approval,
2745
+ execute: async (_id, params) => {
2746
+ if (params?.probe === true)
2747
+ return jsonResult(await hlSubmitAgentApprovalTool(params));
2748
+ const prov = verifyOperatorProvenance(params.operator_token);
2749
+ if (!prov.ok)
2750
+ return jsonResult({ error: prov.error });
2751
+ return jsonResult(await hlSubmitAgentApprovalTool(params));
2752
+ },
2753
+ },
2729
2754
  {
2730
2755
  name: 'hl_agent_wallet_status',
2731
2756
  label: 'Hyperliquid Agent Wallet Status',
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "reefclaw-paper-trading",
3
3
  "name": "ReefClaw Trading",
4
- "version": "0.1.22",
4
+ "version": "0.1.23",
5
5
  "description": "Supervised trading plugin for the ReefClaw dashboard. It runs on YOUR machine and starts in PAPER mode with no API keys. It cannot trade real funds until you supply exchange credentials and step PAPER→MICRO_LIVE→LIVE yourself from the dashboard — the agent cannot make that change (the tool is refused without operator provenance). Exchange keys stay local, are used only to sign requests to the exchange, and are never transmitted to ReefClaw (asserted by a test in this package). Trading telemetry — positions, fills, decision journal — is sent to ReefClaw to render the dashboard. Every live position carries exchange-native protective stops. Remote updates to the agent's trading instructions are applied only after an Ed25519 signature is verified against a public key pinned in this build.",
6
6
  "author": "ReefClaw",
7
7
  "activation": {
@@ -72,6 +72,7 @@
72
72
  "clear_exchange_credentials",
73
73
  "hl_provision_agent_wallet",
74
74
  "hl_agent_wallet_status",
75
+ "hl_submit_agent_approval",
75
76
  "get_bracket_config",
76
77
  "set_bracket_requirement"
77
78
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/openclaw-plugin",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "ReefClaw supervised trading plugin for OpenClaw. Runs entirely on YOUR machine and starts in PAPER mode \u00e2\u20ac\u201d it cannot trade real funds until you supply exchange credentials and walk the PAPER\u00e2\u2020\u2019MICRO_LIVE\u00e2\u2020\u2019LIVE ladder yourself from the ReefClaw dashboard (the agent cannot make that change; it is refused without operator provenance). Your exchange API keys stay on your machine to sign requests to the exchange and are NEVER sent to ReefClaw \u00e2\u20ac\u201d a test in the package asserts this. What does reach ReefClaw is trading telemetry for the dashboard (positions, fills, decision journal). Live trading always carries exchange-native protective stops. Trading instructions can be updated remotely, and every update must carry a valid Ed25519 signature verified against a key pinned in this build before it is applied. Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -96,6 +96,24 @@ export async function hlProvisionAgentWalletTool(args, deps) {
96
96
  const derived = await deriveAddressFromPrivateKey(existingKey);
97
97
  if (derived.ok) {
98
98
  const masterMatches = existingMaster != null && existingMaster.toLowerCase() === walletAddress.toLowerCase();
99
+ // ★ Honour a CHANGED network on the resume path. The same keypair is
100
+ // valid on both Hyperliquid networks, so switching mainnet<->testnet
101
+ // must not require regenerating the wallet — but the stored flag has to
102
+ // follow, or the box boots against the network the operator did NOT
103
+ // pick. Before this, unticking "Use Hyperliquid testnet" after
104
+ // provisioning silently kept testnet:true, and the approval was signed
105
+ // for Testnet while the operator believed they were on mainnet
106
+ // (observed live 2026-08-03).
107
+ if (masterMatches && existingTestnet !== testnet) {
108
+ try {
109
+ updatePluginConfig({ exchange: buildVenueExchangeConfig(existingExchange, 'hyperliquid', {}, testnet) }, deps.configPath);
110
+ existingTestnet = testnet;
111
+ logger.info(TAG, `network switched to ${testnet ? 'TESTNET' : 'MAINNET'} (same agent wallet)`);
112
+ }
113
+ catch (err) {
114
+ logger.warn(TAG, `could not persist network change: ${err instanceof Error ? err.message : String(err)}`);
115
+ }
116
+ }
99
117
  return {
100
118
  ok: true,
101
119
  message: masterMatches
@@ -0,0 +1,27 @@
1
+ export interface HlSubmitAgentApprovalArgs {
2
+ /** The approveAgent action, byte-identical to what the wallet signed. */
3
+ action?: Record<string, unknown>;
4
+ /** Outer nonce — for user-signed actions it must equal action.nonce. */
5
+ nonce?: number;
6
+ signature?: {
7
+ r?: unknown;
8
+ s?: unknown;
9
+ v?: unknown;
10
+ };
11
+ /** Tool-discovery probe — returns immediately, no config or network. */
12
+ probe?: boolean;
13
+ }
14
+ export interface HlSubmitAgentApprovalResult {
15
+ ok: boolean;
16
+ message: string;
17
+ /** Hyperliquid's own status string when it answered ('ok' | 'err'). */
18
+ hlStatus?: string;
19
+ /** Which network the SIGNED action targeted. */
20
+ chain?: 'Mainnet' | 'Testnet';
21
+ agentAddress?: string;
22
+ }
23
+ export interface HlSubmitAgentApprovalDeps {
24
+ configPath?: string;
25
+ fetchImpl?: typeof fetch;
26
+ }
27
+ export declare function hlSubmitAgentApprovalTool(args: HlSubmitAgentApprovalArgs, deps?: HlSubmitAgentApprovalDeps): Promise<HlSubmitAgentApprovalResult>;
@@ -0,0 +1,140 @@
1
+ // hl_submit_agent_approval — operator-only plugin tool.
2
+ //
3
+ // Submits the operator's WALLET-SIGNED `approveAgent` action to Hyperliquid
4
+ // from THIS BOX, instead of from the dashboard's browser.
5
+ //
6
+ // ★ Why the box and not the browser (real failure, 2026-08-03): the guided
7
+ // flow POSTed to api.hyperliquid*.xyz directly from the dashboard page and an
8
+ // operator hit "Failed to fetch" twice. The endpoint was fine (CORS is `*`,
9
+ // POSTs answer normally) and the box reached it in 0.49s — the request was
10
+ // blocked INSIDE the browser (ad-blocker / shield / DNS policy; crypto API
11
+ // domains are routinely blocked). Making a required onboarding step depend on
12
+ // the operator's browser extensions is the wrong architecture: the box is
13
+ // already the Hyperliquid client (every read and every future order goes
14
+ // through it), so it submits. The browser's only irreplaceable job is holding
15
+ // the wallet and producing the signature. ReefClaw's servers stay out of the
16
+ // path — the signed action travels over the existing relay to the user's own
17
+ // machine.
18
+ //
19
+ // ★ SECURITY — this is deliberately NOT a generic signed-action relay.
20
+ // A tool that forwarded any {action, signature} pair to /exchange would be a
21
+ // capability escalation: HL's withdraw / usdSend / usdClassTransfer are all
22
+ // user-signed actions in the same envelope shape. If a signature for one of
23
+ // those ever existed, a generic relay would happily submit it. So:
24
+ // 1. `action.type` MUST be exactly 'approveAgent' — nothing else is accepted.
25
+ // 2. `action.agentAddress` MUST be the address THIS box's provisioned agent
26
+ // key controls, so the tool can only ever authorize our own wallet.
27
+ // 3. The endpoint is chosen from the SIGNED action's `hyperliquidChain`, so
28
+ // it cannot be pointed at a different network than the one signed for.
29
+ // The signature itself is produced and validated by Hyperliquid; we never
30
+ // touch key material here (the agent key is only used to DERIVE its public
31
+ // address for check 2).
32
+ import { readPluginConfig } from '../config/plugin-config-io.js';
33
+ import { deriveAddressFromPrivateKey, isHexAddress, sameAddress } from '../venues/hyperliquid/hl-agent-wallet.js';
34
+ import { hlApiBase } from '../venues/hyperliquid/hl-preflight.js';
35
+ import { logger } from '../logger.js';
36
+ const TAG = 'hl-submit-agent-approval';
37
+ function fail(message, extra = {}) {
38
+ return { ok: false, message, ...extra };
39
+ }
40
+ export async function hlSubmitAgentApprovalTool(args, deps = {}) {
41
+ if (args?.probe === true) {
42
+ return { ok: false, message: 'probe' };
43
+ }
44
+ const action = args?.action;
45
+ if (!action || typeof action !== 'object') {
46
+ return fail('action is required (the approveAgent object the wallet signed)');
47
+ }
48
+ // 1. Only ever approveAgent — never a generic relay (see header).
49
+ if (action.type !== 'approveAgent') {
50
+ logger.warn(TAG, `refused non-approveAgent action type: ${String(action.type)}`);
51
+ return fail(`Refused: this tool only submits 'approveAgent' actions, not '${String(action.type)}'.`);
52
+ }
53
+ const chain = action.hyperliquidChain;
54
+ if (chain !== 'Mainnet' && chain !== 'Testnet') {
55
+ return fail("action.hyperliquidChain must be 'Mainnet' or 'Testnet'");
56
+ }
57
+ const agentAddress = action.agentAddress;
58
+ if (!isHexAddress(agentAddress)) {
59
+ return fail('action.agentAddress must be a 0x… address', { chain });
60
+ }
61
+ const sig = args?.signature;
62
+ const r = sig?.r;
63
+ const s = sig?.s;
64
+ const v = sig?.v;
65
+ if (typeof r !== 'string' || typeof s !== 'string' || typeof v !== 'number') {
66
+ return fail('signature must be { r: string, s: string, v: number }', { chain });
67
+ }
68
+ const nonce = args?.nonce;
69
+ // User-signed actions carry ONE nonce — the outer value must match the one
70
+ // inside the signed payload, or HL verifies a different message than we send.
71
+ if (typeof nonce !== 'number' || nonce !== action.nonce) {
72
+ return fail('nonce must be a number equal to action.nonce', { chain });
73
+ }
74
+ // 2. The agent being approved must be OUR provisioned wallet.
75
+ let storedKey;
76
+ try {
77
+ const cfg = readPluginConfig(deps.configPath);
78
+ if (typeof cfg.exchange?.agentPrivateKey === 'string')
79
+ storedKey = cfg.exchange.agentPrivateKey;
80
+ }
81
+ catch {
82
+ // fall through to the not-provisioned error
83
+ }
84
+ if (!storedKey) {
85
+ return fail('No Hyperliquid agent wallet is provisioned on this machine — run the guided setup first.', { chain });
86
+ }
87
+ const derived = await deriveAddressFromPrivateKey(storedKey);
88
+ if (!derived.ok) {
89
+ return fail('Could not derive this machine’s agent wallet address — cannot verify what is being approved.', { chain });
90
+ }
91
+ if (!sameAddress(derived.address, agentAddress)) {
92
+ logger.warn(TAG, 'refused: action.agentAddress is not this box’s provisioned agent');
93
+ return fail('Refused: the approval targets a different agent wallet than the one provisioned on this machine.', { chain, agentAddress: derived.address });
94
+ }
95
+ // 3. Submit — endpoint from the SIGNED chain, never from local config.
96
+ const fetchImpl = deps.fetchImpl ?? fetch;
97
+ const url = `${hlApiBase(chain === 'Testnet')}/exchange`;
98
+ logger.info(TAG, `submitting approveAgent for ${agentAddress} to ${chain}`);
99
+ try {
100
+ const res = await fetchImpl(url, {
101
+ method: 'POST',
102
+ headers: { 'content-type': 'application/json' },
103
+ body: JSON.stringify({ action, nonce, signature: { r, s, v } }),
104
+ signal: AbortSignal.timeout(20_000),
105
+ });
106
+ const text = await res.text();
107
+ let body = null;
108
+ try {
109
+ body = JSON.parse(text);
110
+ }
111
+ catch {
112
+ // non-JSON — surfaced verbatim below
113
+ }
114
+ if (!res.ok || body?.status !== 'ok') {
115
+ const detail = typeof body?.response === 'string' ? body.response : text.slice(0, 300) || `HTTP ${res.status}`;
116
+ logger.warn(TAG, `Hyperliquid rejected the approval: ${detail}`);
117
+ return fail(`Hyperliquid rejected the approval: ${detail}`, {
118
+ hlStatus: body?.status,
119
+ chain,
120
+ agentAddress: derived.address,
121
+ });
122
+ }
123
+ logger.info(TAG, `approveAgent accepted by Hyperliquid (${chain})`);
124
+ return {
125
+ ok: true,
126
+ message: `Approval submitted and accepted by Hyperliquid (${chain}).`,
127
+ hlStatus: 'ok',
128
+ chain,
129
+ agentAddress: derived.address,
130
+ };
131
+ }
132
+ catch (err) {
133
+ const msg = err instanceof Error ? err.message : String(err);
134
+ logger.error(TAG, `submit failed: ${msg}`);
135
+ return fail(`Could not reach Hyperliquid from this machine: ${msg}`, {
136
+ chain,
137
+ agentAddress: derived.address,
138
+ });
139
+ }
140
+ }