@reefclaw/openclaw-plugin 0.1.19 → 0.1.21
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.d.ts +10 -0
- package/bridge/bridge.js +142 -22
- package/bridge/gateway/gateway-ws-client.d.ts +7 -1
- package/bridge/gateway/gateway-ws-client.js +31 -8
- package/bridge/gateway/tool-discovery.d.ts +1 -1
- package/bridge/gateway/tool-discovery.js +11 -0
- package/bridge/provider.d.ts +73 -6
- package/bridge/providers/gateway.d.ts +12 -3
- package/bridge/providers/gateway.js +18 -5
- package/bridge/providers/onboarding-commands.d.ts +33 -7
- package/bridge/providers/onboarding-commands.js +109 -13
- package/bridge/types.d.ts +1 -1
- package/bridge/types.js +5 -0
- package/ccxt/binance-private.js +6 -0
- package/config/tool-gate.js +4 -0
- package/index.js +77 -12
- package/ingest/position-auto-capture.js +19 -0
- package/ingest/readiness-reporter.js +25 -1
- package/openclaw.plugin.json +3 -1
- package/package.json +38 -38
- package/security/sealed-credentials.d.ts +38 -0
- package/security/sealed-credentials.js +180 -0
- package/tools/clear-exchange-credentials.js +16 -3
- package/tools/hl-agent-wallet-status.d.ts +29 -0
- package/tools/hl-agent-wallet-status.js +100 -0
- package/tools/hl-provision-agent-wallet.d.ts +35 -0
- package/tools/hl-provision-agent-wallet.js +144 -0
- package/tools/set-exchange-credentials.d.ts +32 -3
- package/tools/set-exchange-credentials.js +144 -32
- package/tools/set-trading-mode.d.ts +7 -0
- package/tools/set-trading-mode.js +15 -0
- package/tools/test-exchange-credentials.d.ts +30 -3
- package/tools/test-exchange-credentials.js +173 -35
- package/types.d.ts +10 -11
- package/venues/hyperliquid/hl-agent-wallet.d.ts +29 -0
- package/venues/hyperliquid/hl-agent-wallet.js +118 -0
- package/venues/hyperliquid/hl-preflight.d.ts +28 -0
- package/venues/hyperliquid/hl-preflight.js +116 -0
|
@@ -38,11 +38,54 @@ export async function executeSetTradingMode(ctx, mode, acknowledged) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/** Build the plugin-tool payload for a per-venue credential request. The
|
|
42
|
+
* secret material (Binance secret / HL agent key) passes through verbatim
|
|
43
|
+
* exactly once; the second return value is what must be scrubbed from any
|
|
44
|
+
* error text before it can be logged. */
|
|
45
|
+
function credentialPayload(ctx, req) {
|
|
46
|
+
if ('sealed' in req) {
|
|
47
|
+
// End-to-end encrypted envelope — forwarded opaquely; there is no
|
|
48
|
+
// plaintext here to scrub (the empty scrub token never matches anything:
|
|
49
|
+
// executeSet/Test guard on it before building a scrub regex).
|
|
50
|
+
return {
|
|
51
|
+
payload: { sealed: req.sealed, operator_token: ctx.operatorToken },
|
|
52
|
+
scrub: '',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (req.venue === 'hyperliquid') {
|
|
56
|
+
return {
|
|
57
|
+
payload: {
|
|
58
|
+
venue: 'hyperliquid',
|
|
59
|
+
walletAddress: req.walletAddress,
|
|
60
|
+
agentPrivateKey: req.agentPrivateKey,
|
|
61
|
+
testnet: req.testnet,
|
|
62
|
+
operator_token: ctx.operatorToken,
|
|
63
|
+
},
|
|
64
|
+
scrub: req.agentPrivateKey,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
payload: {
|
|
69
|
+
venue: 'binance',
|
|
70
|
+
apiKey: req.apiKey,
|
|
71
|
+
secret: req.secret,
|
|
72
|
+
testnet: req.testnet,
|
|
73
|
+
operator_token: ctx.operatorToken,
|
|
74
|
+
},
|
|
75
|
+
scrub: req.secret,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function scrubError(reason, scrub) {
|
|
79
|
+
if (!scrub)
|
|
80
|
+
return reason;
|
|
81
|
+
return reason.replace(new RegExp(escapeRegex(scrub), 'g'), '***');
|
|
82
|
+
}
|
|
41
83
|
/** Forward a test_exchange_credentials invocation to the plugin. The plugin
|
|
42
|
-
* does
|
|
43
|
-
*
|
|
44
|
-
* (same approach as the set path). */
|
|
45
|
-
export async function executeTestExchangeCredentials(ctx,
|
|
84
|
+
* does throwaway read-only calls to verify the credentials without
|
|
85
|
+
* persisting them. Error messages are scrubbed of the raw secret / agent key
|
|
86
|
+
* before logging (same approach as the set path). */
|
|
87
|
+
export async function executeTestExchangeCredentials(ctx, req) {
|
|
88
|
+
const testnet = ('testnet' in req ? req.testnet : undefined) ?? false;
|
|
46
89
|
const tool = ctx.toolMap.test_exchange_credentials;
|
|
47
90
|
if (!tool || !ctx.http) {
|
|
48
91
|
return {
|
|
@@ -52,17 +95,18 @@ export async function executeTestExchangeCredentials(ctx, apiKey, secret, testne
|
|
|
52
95
|
canReadBalance: false,
|
|
53
96
|
canReadPositions: false,
|
|
54
97
|
balanceUSDT: null,
|
|
55
|
-
testnet
|
|
98
|
+
testnet,
|
|
56
99
|
errors: ['tool-not-available'],
|
|
57
100
|
};
|
|
58
101
|
}
|
|
102
|
+
const { payload, scrub } = credentialPayload(ctx, req);
|
|
59
103
|
try {
|
|
60
|
-
const result = await ctx.http.invoke(tool,
|
|
104
|
+
const result = await ctx.http.invoke(tool, payload);
|
|
61
105
|
return result.data;
|
|
62
106
|
}
|
|
63
107
|
catch (err) {
|
|
64
108
|
const reason = err instanceof Error ? err.message : String(err);
|
|
65
|
-
const sanitized = reason
|
|
109
|
+
const sanitized = scrubError(reason, scrub);
|
|
66
110
|
logger.error(TAG, `test_exchange_credentials failed: ${sanitized}`);
|
|
67
111
|
return {
|
|
68
112
|
ok: false,
|
|
@@ -71,7 +115,7 @@ export async function executeTestExchangeCredentials(ctx, apiKey, secret, testne
|
|
|
71
115
|
canReadBalance: false,
|
|
72
116
|
canReadPositions: false,
|
|
73
117
|
balanceUSDT: null,
|
|
74
|
-
testnet
|
|
118
|
+
testnet,
|
|
75
119
|
errors: [sanitized],
|
|
76
120
|
};
|
|
77
121
|
}
|
|
@@ -177,11 +221,62 @@ export async function executeSetBracketRequirement(ctx, flag, value) {
|
|
|
177
221
|
};
|
|
178
222
|
}
|
|
179
223
|
}
|
|
224
|
+
/** Forward an hl_provision_agent_wallet invocation to the plugin. No secrets
|
|
225
|
+
* transit this path in EITHER direction — the request carries a public
|
|
226
|
+
* master address; the response carries a public agent address (the private
|
|
227
|
+
* key is minted and stays on the box). */
|
|
228
|
+
export async function executeProvisionHlAgentWallet(ctx, args) {
|
|
229
|
+
const tool = ctx.toolMap.hl_provision_agent_wallet;
|
|
230
|
+
if (!tool || !ctx.http) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
message: 'hl_provision_agent_wallet tool not available on gateway — update the ReefClaw plugin (npx @reefclaw/connect@latest)',
|
|
234
|
+
venue: 'hyperliquid',
|
|
235
|
+
mode: 'PAPER',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const result = await ctx.http.invoke(tool, {
|
|
240
|
+
walletAddress: args.walletAddress,
|
|
241
|
+
testnet: args.testnet,
|
|
242
|
+
regenerate: args.regenerate,
|
|
243
|
+
operator_token: ctx.operatorToken,
|
|
244
|
+
});
|
|
245
|
+
return result.data;
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
249
|
+
logger.error(TAG, `hl_provision_agent_wallet failed: ${reason}`);
|
|
250
|
+
return { ok: false, message: reason, venue: 'hyperliquid', mode: 'PAPER' };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Forward an hl_agent_wallet_status invocation to the plugin. Read-only. */
|
|
254
|
+
export async function executeHlAgentWalletStatus(ctx) {
|
|
255
|
+
const tool = ctx.toolMap.hl_agent_wallet_status;
|
|
256
|
+
if (!tool || !ctx.http) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
message: 'hl_agent_wallet_status tool not available on gateway — update the ReefClaw plugin (npx @reefclaw/connect@latest)',
|
|
260
|
+
configured: false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const result = await ctx.http.invoke(tool, {
|
|
265
|
+
operator_token: ctx.operatorToken,
|
|
266
|
+
});
|
|
267
|
+
return result.data;
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
271
|
+
logger.error(TAG, `hl_agent_wallet_status failed: ${reason}`);
|
|
272
|
+
return { ok: false, message: reason, configured: false };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
180
275
|
/** Forward a set_exchange_credentials invocation to the plugin via HTTP.
|
|
181
|
-
* The raw secret is sent once over TLS and never retained here —
|
|
182
|
-
* caching, no log lines with the raw value. Errors are scrubbed of the
|
|
276
|
+
* The raw secret / agent key is sent once over TLS and never retained here —
|
|
277
|
+
* no caching, no log lines with the raw value. Errors are scrubbed of the
|
|
183
278
|
* secret before logging in case it ever appears in an error chain. */
|
|
184
|
-
export async function executeSetExchangeCredentials(ctx,
|
|
279
|
+
export async function executeSetExchangeCredentials(ctx, req) {
|
|
185
280
|
const tool = ctx.toolMap.set_exchange_credentials;
|
|
186
281
|
if (!tool || !ctx.http) {
|
|
187
282
|
return {
|
|
@@ -193,13 +288,14 @@ export async function executeSetExchangeCredentials(ctx, apiKey, secret, testnet
|
|
|
193
288
|
readiness: 'UNKNOWN',
|
|
194
289
|
};
|
|
195
290
|
}
|
|
291
|
+
const { payload, scrub } = credentialPayload(ctx, req);
|
|
196
292
|
try {
|
|
197
|
-
const result = await ctx.http.invoke(tool,
|
|
293
|
+
const result = await ctx.http.invoke(tool, payload);
|
|
198
294
|
return result.data;
|
|
199
295
|
}
|
|
200
296
|
catch (err) {
|
|
201
297
|
const reason = err instanceof Error ? err.message : String(err);
|
|
202
|
-
const sanitized = reason
|
|
298
|
+
const sanitized = scrubError(reason, scrub);
|
|
203
299
|
logger.error(TAG, `set_exchange_credentials failed: ${sanitized}`);
|
|
204
300
|
return {
|
|
205
301
|
ok: false,
|
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", "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", "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
|
@@ -18,6 +18,9 @@ export const ALLOWED_METHODS = [
|
|
|
18
18
|
'set_exchange_credentials',
|
|
19
19
|
'test_exchange_credentials',
|
|
20
20
|
'clear_exchange_credentials',
|
|
21
|
+
// Hyperliquid guided onboarding (box-generated agent wallet) — operator.write-gated.
|
|
22
|
+
'hl_provision_agent_wallet',
|
|
23
|
+
'hl_agent_wallet_status',
|
|
21
24
|
// Bracket-orders config (Phase 3.5b) — operator.write-gated.
|
|
22
25
|
'get_bracket_config',
|
|
23
26
|
'set_bracket_requirement',
|
|
@@ -33,6 +36,8 @@ export const OPERATOR_WRITE_METHODS = new Set([
|
|
|
33
36
|
'set_exchange_credentials',
|
|
34
37
|
'test_exchange_credentials',
|
|
35
38
|
'clear_exchange_credentials',
|
|
39
|
+
'hl_provision_agent_wallet',
|
|
40
|
+
'hl_agent_wallet_status',
|
|
36
41
|
'get_bracket_config',
|
|
37
42
|
'set_bracket_requirement',
|
|
38
43
|
'emergency.kill',
|
package/ccxt/binance-private.js
CHANGED
|
@@ -73,6 +73,12 @@ export class BinancePrivateApi {
|
|
|
73
73
|
exchange;
|
|
74
74
|
testnet;
|
|
75
75
|
constructor(credentials) {
|
|
76
|
+
// apiKey/secret are optional on ExchangeConfig (an HL block carries a
|
|
77
|
+
// wallet pair instead) — this class is Binance-only and must never be
|
|
78
|
+
// constructed without the HMAC pair (mirrors HyperliquidPrivateApi's guard).
|
|
79
|
+
if (!credentials.apiKey || !credentials.secret) {
|
|
80
|
+
throw new Error('BinancePrivateApi requires exchange.apiKey + exchange.secret');
|
|
81
|
+
}
|
|
76
82
|
this.testnet = credentials.testnet ?? false;
|
|
77
83
|
const BinanceClass = ccxtCjs.binance ?? ccxtCjs.default?.binance;
|
|
78
84
|
if (!BinanceClass) {
|
package/config/tool-gate.js
CHANGED
|
@@ -31,6 +31,10 @@ export const UNGOVERNABLE_TOOLS = new Set([
|
|
|
31
31
|
'set_trading_mode',
|
|
32
32
|
'get_bracket_config',
|
|
33
33
|
'set_bracket_requirement',
|
|
34
|
+
// Hyperliquid guided onboarding (box-generated agent wallet) — same
|
|
35
|
+
// operator-only channel; disabling them would strand the HL setup flow.
|
|
36
|
+
'hl_provision_agent_wallet',
|
|
37
|
+
'hl_agent_wallet_status',
|
|
34
38
|
]);
|
|
35
39
|
/** RC_TOOL_GATE: 'off' = kill-switch (gate never blocks), 'shadow' = log
|
|
36
40
|
* would-be blocks but execute anyway, anything else/default = 'enforce'.
|
package/index.js
CHANGED
|
@@ -66,6 +66,9 @@ import { getBracketConfigTool } from './tools/get-bracket-config.js';
|
|
|
66
66
|
import { setBracketRequirementTool } from './tools/set-bracket-requirement.js';
|
|
67
67
|
import { testExchangeCredentialsTool } from './tools/test-exchange-credentials.js';
|
|
68
68
|
import { clearExchangeCredentialsTool } from './tools/clear-exchange-credentials.js';
|
|
69
|
+
import { hlProvisionAgentWalletTool } from './tools/hl-provision-agent-wallet.js';
|
|
70
|
+
import { hlAgentWalletStatusTool } from './tools/hl-agent-wallet-status.js';
|
|
71
|
+
import { ensureCredentialTransportKey } from './security/sealed-credentials.js';
|
|
69
72
|
// Tool implementations
|
|
70
73
|
import { fetchTickerTool } from './tools/fetch-ticker.js';
|
|
71
74
|
import { fetchOhlcvTool } from './tools/fetch-ohlcv.js';
|
|
@@ -798,11 +801,14 @@ const TOOL_PARAMS = {
|
|
|
798
801
|
type: 'object',
|
|
799
802
|
properties: {
|
|
800
803
|
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
+
venue: { type: 'string', enum: ['binance', 'hyperliquid'], description: "Trading venue the credentials are for (default 'binance')" },
|
|
805
|
+
apiKey: { type: 'string', description: 'Binance API key (venue=binance)' },
|
|
806
|
+
secret: { type: 'string', description: 'Binance API secret (venue=binance)' },
|
|
807
|
+
walletAddress: { type: 'string', description: 'Hyperliquid MASTER account address 0x… (venue=hyperliquid; never a private key)' },
|
|
808
|
+
agentPrivateKey: { type: 'string', description: 'Hyperliquid AGENT (API) wallet private key — signs only, cannot withdraw (venue=hyperliquid)' },
|
|
809
|
+
testnet: { type: 'boolean', description: 'Use the venue testnet (default: false)' },
|
|
810
|
+
sealed: { type: 'object', description: 'End-to-end encrypted credential envelope from the dashboard (replaces the plaintext fields; only this box can open it)' },
|
|
804
811
|
},
|
|
805
|
-
required: ['apiKey', 'secret'],
|
|
806
812
|
},
|
|
807
813
|
set_trading_mode: {
|
|
808
814
|
type: 'object',
|
|
@@ -817,11 +823,30 @@ const TOOL_PARAMS = {
|
|
|
817
823
|
type: 'object',
|
|
818
824
|
properties: {
|
|
819
825
|
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
826
|
+
venue: { type: 'string', enum: ['binance', 'hyperliquid'], description: "Trading venue to verify against (default 'binance')" },
|
|
827
|
+
apiKey: { type: 'string', description: 'Binance API key to verify (venue=binance; not persisted)' },
|
|
828
|
+
secret: { type: 'string', description: 'Binance API secret to verify (venue=binance; not persisted)' },
|
|
829
|
+
walletAddress: { type: 'string', description: 'Hyperliquid MASTER account address 0x… to verify (venue=hyperliquid; not persisted)' },
|
|
830
|
+
agentPrivateKey: { type: 'string', description: 'Hyperliquid AGENT wallet private key to verify (venue=hyperliquid; not persisted)' },
|
|
831
|
+
testnet: { type: 'boolean', description: 'Test against the venue testnet' },
|
|
832
|
+
sealed: { type: 'object', description: 'End-to-end encrypted credential envelope from the dashboard (replaces the plaintext fields; only this box can open it)' },
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
hl_provision_agent_wallet: {
|
|
836
|
+
type: 'object',
|
|
837
|
+
properties: {
|
|
838
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
839
|
+
walletAddress: { type: 'string', description: 'Hyperliquid MASTER account address 0x… the agent wallet will trade for (public; never a private key)' },
|
|
840
|
+
testnet: { type: 'boolean', description: 'Provision for the Hyperliquid testnet (default: false)' },
|
|
841
|
+
regenerate: { type: 'boolean', description: 'Replace an existing agent wallet with a fresh key (PAPER mode only; the old approval is orphaned)' },
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
hl_agent_wallet_status: {
|
|
845
|
+
type: 'object',
|
|
846
|
+
properties: {
|
|
847
|
+
operator_token: { type: 'string', description: 'Operator provenance — injected automatically by the ReefClaw dashboard path. Agent-initiated calls are refused without it.' },
|
|
848
|
+
probe: { type: 'boolean', description: 'Tool-discovery probe — returns immediately without config or network access' },
|
|
823
849
|
},
|
|
824
|
-
required: ['apiKey', 'secret'],
|
|
825
850
|
},
|
|
826
851
|
clear_exchange_credentials: {
|
|
827
852
|
type: 'object',
|
|
@@ -2639,13 +2664,13 @@ const paperTradingPlugin = {
|
|
|
2639
2664
|
{
|
|
2640
2665
|
name: 'set_exchange_credentials',
|
|
2641
2666
|
label: 'Set Exchange Credentials',
|
|
2642
|
-
description: 'Operator-only. Store
|
|
2667
|
+
description: 'Operator-only. Store exchange credentials in ~/.reefclaw/plugin-config.json — Binance API key + secret, or Hyperliquid master address + agent wallet key (venue-selected). Same-venue live modes reconnect the adapter; a venue CHANGE is stored but requires an agent restart. Refused without dashboard operator provenance.',
|
|
2643
2668
|
parameters: TOOL_PARAMS.set_exchange_credentials,
|
|
2644
2669
|
execute: async (_id, params) => {
|
|
2645
2670
|
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2646
2671
|
if (!prov.ok)
|
|
2647
2672
|
return jsonResult({ error: prov.error });
|
|
2648
|
-
return jsonResult(await setExchangeCredentialsTool(params, { runtime, adapterDeps }));
|
|
2673
|
+
return jsonResult(await setExchangeCredentialsTool(params, { runtime, adapterDeps, bootVenue: venue }));
|
|
2649
2674
|
},
|
|
2650
2675
|
},
|
|
2651
2676
|
{
|
|
@@ -2657,13 +2682,13 @@ const paperTradingPlugin = {
|
|
|
2657
2682
|
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2658
2683
|
if (!prov.ok)
|
|
2659
2684
|
return jsonResult({ error: prov.error });
|
|
2660
|
-
return jsonResult(await setTradingModeTool(params, { runtime, adapterDeps }));
|
|
2685
|
+
return jsonResult(await setTradingModeTool(params, { runtime, adapterDeps, bootVenue: venue }));
|
|
2661
2686
|
},
|
|
2662
2687
|
},
|
|
2663
2688
|
{
|
|
2664
2689
|
name: 'test_exchange_credentials',
|
|
2665
2690
|
label: 'Test Exchange Credentials',
|
|
2666
|
-
description: 'Operator-only. Verify
|
|
2691
|
+
description: 'Operator-only. Verify candidate exchange credentials with read-only calls, per venue (Binance: fetchBalance permissions; Hyperliquid: master account state + agent-approval check), without persisting anything. Used by the dashboard pre-flight check before set_exchange_credentials. Refused without dashboard operator provenance.',
|
|
2667
2692
|
parameters: TOOL_PARAMS.test_exchange_credentials,
|
|
2668
2693
|
// Read-only, but provenance-gated anyway: it makes a live authenticated
|
|
2669
2694
|
// exchange call with caller-supplied keys, which would otherwise hand a
|
|
@@ -2687,6 +2712,35 @@ const paperTradingPlugin = {
|
|
|
2687
2712
|
return jsonResult(await clearExchangeCredentialsTool(params, { runtime, adapterDeps }));
|
|
2688
2713
|
},
|
|
2689
2714
|
},
|
|
2715
|
+
{
|
|
2716
|
+
name: 'hl_provision_agent_wallet',
|
|
2717
|
+
label: 'Provision Hyperliquid Agent Wallet',
|
|
2718
|
+
description: 'Operator-only. Generate a Hyperliquid agent-wallet keypair ON THIS MACHINE (the private key never leaves it, nobody ever sees it) and return only the public address for the operator to approve on Hyperliquid. Idempotent — an existing wallet is returned, not replaced, unless {regenerate: true} (PAPER mode only). Refused without dashboard operator provenance.',
|
|
2719
|
+
parameters: TOOL_PARAMS.hl_provision_agent_wallet,
|
|
2720
|
+
execute: async (_id, params) => {
|
|
2721
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2722
|
+
if (!prov.ok)
|
|
2723
|
+
return jsonResult({ error: prov.error });
|
|
2724
|
+
return jsonResult(await hlProvisionAgentWalletTool(params, { runtime, bootVenue: venue }));
|
|
2725
|
+
},
|
|
2726
|
+
},
|
|
2727
|
+
{
|
|
2728
|
+
name: 'hl_agent_wallet_status',
|
|
2729
|
+
label: 'Hyperliquid Agent Wallet Status',
|
|
2730
|
+
description: 'Operator-only. Report the provisioned Hyperliquid agent wallet (public address, approval status via extraAgents, expiry, master balance). The dashboard polls this after the operator signs the approval. Read-only, keyless exchange queries. Refused without dashboard operator provenance.',
|
|
2731
|
+
parameters: TOOL_PARAMS.hl_agent_wallet_status,
|
|
2732
|
+
execute: async (_id, params) => {
|
|
2733
|
+
// The probe path must answer BEFORE the provenance gate — tool
|
|
2734
|
+
// discovery has no operator token, and a provenance refusal is an
|
|
2735
|
+
// envelope-level error the prober can't tell apart from execution.
|
|
2736
|
+
if (params?.probe === true)
|
|
2737
|
+
return jsonResult(await hlAgentWalletStatusTool(params, {}));
|
|
2738
|
+
const prov = verifyOperatorProvenance(params.operator_token);
|
|
2739
|
+
if (!prov.ok)
|
|
2740
|
+
return jsonResult({ error: prov.error });
|
|
2741
|
+
return jsonResult(await hlAgentWalletStatusTool(params, { bootVenue: venue }));
|
|
2742
|
+
},
|
|
2743
|
+
},
|
|
2690
2744
|
{
|
|
2691
2745
|
name: 'get_bracket_config',
|
|
2692
2746
|
label: 'Get Bracket Config',
|
|
@@ -2778,6 +2832,17 @@ const paperTradingPlugin = {
|
|
|
2778
2832
|
// instead of a false green. Probes ONLY the configured venue — on a
|
|
2779
2833
|
// Binance-451 host trading Hyperliquid a Binance probe would be a
|
|
2780
2834
|
// permanent false alarm. Advisory + fire-and-forget; no token → no-op.
|
|
2835
|
+
// Sealed credential transport (security/sealed-credentials.ts): make sure
|
|
2836
|
+
// this box's static keypair exists BEFORE the first readiness report goes
|
|
2837
|
+
// out — the report carries the PUBLIC half so the dashboard can encrypt
|
|
2838
|
+
// credential payloads that only this box can open. Non-fatal: without a
|
|
2839
|
+
// key the dashboard falls back to the legacy plaintext-over-TLS path.
|
|
2840
|
+
try {
|
|
2841
|
+
ensureCredentialTransportKey();
|
|
2842
|
+
}
|
|
2843
|
+
catch (err) {
|
|
2844
|
+
logger.warn(TAG, `credential transport key setup failed (plaintext fallback stays available): ${formatError(err)}`);
|
|
2845
|
+
}
|
|
2781
2846
|
// Runs here once (guarded by the pluginInitialised early-return → once per
|
|
2782
2847
|
// process) + on an unref'd interval inside the reporter.
|
|
2783
2848
|
startReadinessReporter({
|
|
@@ -124,6 +124,25 @@ export async function onCreateOrderFilled(ctx, inputs, order) {
|
|
|
124
124
|
// scale-ins with WS-exact data; capturing here would double-post).
|
|
125
125
|
const existing = ctx.stateStore.get(inputs.symbol);
|
|
126
126
|
if (existing) {
|
|
127
|
+
// ★ SAME-ORDER GUARD — the twin of the dedup in onWsFillObserved (which
|
|
128
|
+
// returns when `stateEntry.openedFromExchangeTradeId === fill.exchangeOrderId`).
|
|
129
|
+
// That one covers create_order-then-WS; this covers WS-then-create_order,
|
|
130
|
+
// which is the ORDINARY ordering for a market order: the WS fill routinely
|
|
131
|
+
// beats the REST response (measured ~270-415ms on prod).
|
|
132
|
+
//
|
|
133
|
+
// Without it the stale probe below decides this case, and it CANNOT: its
|
|
134
|
+
// test is "does the exchange position equal this fill?", which is equally
|
|
135
|
+
// true of a genuinely stale mapping AND of the mapping the WS path just
|
|
136
|
+
// wrote for THIS VERY FILL. It chose 'stale', dropped the fresh mapping and
|
|
137
|
+
// journaled a second position — 38 duplicate pairs in 9 days (~11/day), each
|
|
138
|
+
// orphaning the WS row at 0 reviews while the exchange held ONE position.
|
|
139
|
+
// The twins were undedupable downstream because they key on different ids
|
|
140
|
+
// (WS = trade id, here = order id).
|
|
141
|
+
const orderId = typeof order.id === 'string' ? order.id : undefined;
|
|
142
|
+
if (orderId && existing.openedFromExchangeTradeId === orderId) {
|
|
143
|
+
logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: WS fill path already captured this order (${orderId}) — skipping duplicate capture`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
127
146
|
const probe = await probeStaleStateEntry(ctx, inputs.symbol, filledQty);
|
|
128
147
|
if (probe === 'stale') {
|
|
129
148
|
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: state-store entry (openedAt=${existing.openedAt}) is STALE — ` +
|
|
@@ -214,12 +214,36 @@ export async function collectReadiness(opts, bootWarmup = false, deps = {}) {
|
|
|
214
214
|
checks.push(makeReadinessCheck(reachId, 'unknown', { checkedAt: now }));
|
|
215
215
|
checks.push(makeReadinessCheck('clock_in_sync', 'unknown', { checkedAt: now }));
|
|
216
216
|
}
|
|
217
|
+
// Credential transport PUBLIC key (security/sealed-credentials.ts): riding
|
|
218
|
+
// the readiness report is what lets the dashboard encrypt credential
|
|
219
|
+
// payloads that only this box can open. Publishing the public half is the
|
|
220
|
+
// key's entire purpose — this is NOT an egress of secret material. Absent
|
|
221
|
+
// key (setup failed) → field omitted → dashboard falls back to plaintext.
|
|
222
|
+
let credentialPublicKey;
|
|
223
|
+
let credentialKeyId;
|
|
224
|
+
try {
|
|
225
|
+
const { getCredentialTransportPublicKey } = await import('../security/sealed-credentials.js');
|
|
226
|
+
const info = getCredentialTransportPublicKey();
|
|
227
|
+
if (info) {
|
|
228
|
+
credentialPublicKey = info.publicKeyBase64;
|
|
229
|
+
credentialKeyId = info.keyId;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Best-effort — readiness must never fail on the transport key.
|
|
234
|
+
}
|
|
217
235
|
return {
|
|
218
236
|
schemaVersion: 1,
|
|
219
237
|
generatedAt: now,
|
|
220
238
|
overall: deriveOverallReadiness(checks),
|
|
221
239
|
checks,
|
|
222
|
-
agent: {
|
|
240
|
+
agent: {
|
|
241
|
+
pluginVersion: PLUGIN_VERSION,
|
|
242
|
+
toolCount: opts.toolCount,
|
|
243
|
+
venue: opts.venue,
|
|
244
|
+
...(credentialPublicKey ? { credentialPublicKey } : {}),
|
|
245
|
+
...(credentialKeyId ? { credentialKeyId } : {}),
|
|
246
|
+
},
|
|
223
247
|
};
|
|
224
248
|
}
|
|
225
249
|
async function postReadiness(apiBaseUrl, token, report, fetchImpl, timeoutMs) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "reefclaw-paper-trading",
|
|
3
3
|
"name": "ReefClaw Trading",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.21",
|
|
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": {
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
"set_trading_mode",
|
|
71
71
|
"test_exchange_credentials",
|
|
72
72
|
"clear_exchange_credentials",
|
|
73
|
+
"hl_provision_agent_wallet",
|
|
74
|
+
"hl_agent_wallet_status",
|
|
73
75
|
"get_bracket_config",
|
|
74
76
|
"set_bracket_requirement"
|
|
75
77
|
]
|
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "ReefClaw supervised trading plugin for OpenClaw. Runs entirely on YOUR machine and starts in PAPER mode
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"openclaw": {
|
|
8
|
-
"extensions": [
|
|
9
|
-
"./index.js"
|
|
10
|
-
],
|
|
11
|
-
"compat": {
|
|
12
|
-
"pluginApi": ">=2026.6.0"
|
|
13
|
-
},
|
|
14
|
-
"build": {
|
|
15
|
-
"openclawVersion": "2026.6.11"
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"**/*",
|
|
20
|
-
"!scripts/**"
|
|
21
|
-
],
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=20"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"@reefclaw/shared": "0.1.4",
|
|
27
|
-
"ccxt": "4.5.37",
|
|
28
|
-
"json5": "2.2.3",
|
|
29
|
-
"ws": "8.21.1"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "node scripts/assemble.mjs",
|
|
33
|
-
"verify": "node scripts/verify-shared-contract.mjs",
|
|
34
|
-
"prepublishOnly": "node scripts/verify-shared-contract.mjs"
|
|
35
|
-
},
|
|
36
|
-
"license": "MIT",
|
|
37
|
-
"homepage": "https://reefclaw.com"
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
+
"version": "0.1.21",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./index.js"
|
|
10
|
+
],
|
|
11
|
+
"compat": {
|
|
12
|
+
"pluginApi": ">=2026.6.0"
|
|
13
|
+
},
|
|
14
|
+
"build": {
|
|
15
|
+
"openclawVersion": "2026.6.11"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"**/*",
|
|
20
|
+
"!scripts/**"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@reefclaw/shared": "0.1.4",
|
|
27
|
+
"ccxt": "4.5.37",
|
|
28
|
+
"json5": "2.2.3",
|
|
29
|
+
"ws": "8.21.1"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node scripts/assemble.mjs",
|
|
33
|
+
"verify": "node scripts/verify-shared-contract.mjs",
|
|
34
|
+
"prepublishOnly": "node scripts/verify-shared-contract.mjs"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://reefclaw.com"
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare class SealedEnvelopeError extends Error {
|
|
2
|
+
/** 'key_mismatch' → the browser sealed to a stale/foreign key (refresh the
|
|
3
|
+
* dashboard so it refetches this box's current key); 'malformed' → not a
|
|
4
|
+
* v1 envelope; 'decrypt_failed' → tampered or corrupted in transit. */
|
|
5
|
+
readonly reason: 'key_mismatch' | 'malformed' | 'decrypt_failed';
|
|
6
|
+
constructor(message: string,
|
|
7
|
+
/** 'key_mismatch' → the browser sealed to a stale/foreign key (refresh the
|
|
8
|
+
* dashboard so it refetches this box's current key); 'malformed' → not a
|
|
9
|
+
* v1 envelope; 'decrypt_failed' → tampered or corrupted in transit. */
|
|
10
|
+
reason: 'key_mismatch' | 'malformed' | 'decrypt_failed');
|
|
11
|
+
}
|
|
12
|
+
export interface SealedEnvelope {
|
|
13
|
+
v: 1;
|
|
14
|
+
/** First 16 hex chars of SHA-256(recipient public key, 65-byte uncompressed). */
|
|
15
|
+
kid: string;
|
|
16
|
+
/** Sender's ephemeral public key — base64, 65-byte uncompressed point. */
|
|
17
|
+
epk: string;
|
|
18
|
+
/** base64, 12 bytes. */
|
|
19
|
+
iv: string;
|
|
20
|
+
/** base64, ciphertext with the 16-byte GCM tag appended. */
|
|
21
|
+
ct: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function computeKeyId(publicKeyRaw: Buffer): string;
|
|
24
|
+
export interface TransportKeyInfo {
|
|
25
|
+
publicKeyBase64: string;
|
|
26
|
+
keyId: string;
|
|
27
|
+
}
|
|
28
|
+
/** Ensure the box's static transport keypair exists (generate + persist 0600 on
|
|
29
|
+
* first run) and return the PUBLIC half for the readiness report. Never
|
|
30
|
+
* returns or logs the private half. */
|
|
31
|
+
export declare function ensureCredentialTransportKey(dir?: string): TransportKeyInfo;
|
|
32
|
+
/** The PUBLIC half for the readiness report, or null when no key exists yet
|
|
33
|
+
* (never generates — boot calls ensureCredentialTransportKey first). */
|
|
34
|
+
export declare function getCredentialTransportPublicKey(dir?: string): TransportKeyInfo | null;
|
|
35
|
+
/** Open a sealed credential envelope with this box's private transport key.
|
|
36
|
+
* Returns the decrypted JSON payload (plain object). Throws
|
|
37
|
+
* SealedEnvelopeError with a caller-renderable reason on any failure. */
|
|
38
|
+
export declare function unsealCredentials(envelope: unknown, dir?: string): Record<string, unknown>;
|