@reefclaw/connect 0.1.33 → 0.1.35

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.
@@ -34,12 +34,31 @@ import { parseVenue } from '../venues/registry.js';
34
34
  import { unsealCredentials, SealedEnvelopeError } from '../security/sealed-credentials.js';
35
35
  import { logger } from '../logger.js';
36
36
  const TAG = 'set-exchange-credentials';
37
+ /** Non-secret CONTROL flags honored from the PLAINTEXT SIBLINGS of a sealed
38
+ * envelope (E2E audit 2026-08-11 #5). The envelope's anti-smuggle property —
39
+ * "plaintext siblings are ignored" — exists so a mixed payload can't override
40
+ * sealed CREDENTIAL values. But the dashboard sends `confirm_venue_switch`
41
+ * as a sibling NEXT TO `sealed` (it is not a secret and the sealer encrypts
42
+ * only the per-venue credential payload), so dropping every sibling made the
43
+ * confirm retry look unconfirmed forever: any live venue-switcher on a box
44
+ * with a transport key hit an infinite confirm loop. The flags below are
45
+ * safe to honor from plaintext because they cannot change WHAT gets stored
46
+ * (the sealed payload alone decides that) — they only acknowledge a warn
47
+ * step for the request the operator themselves sealed. A value INSIDE the
48
+ * envelope still wins (sealed-beats-plaintext is preserved). */
49
+ const SEALED_SIBLING_CONTROL_FLAGS = ['confirm_venue_switch'];
37
50
  /** Open a sealed envelope into plain args, or return a renderable error. */
38
51
  export function resolveSealedArgs(args, configDir) {
39
52
  if (args?.sealed == null)
40
53
  return { args, sealed: false };
41
54
  try {
42
55
  const plain = unsealCredentials(args.sealed, configDir);
56
+ for (const flag of SEALED_SIBLING_CONTROL_FLAGS) {
57
+ const sibling = args[flag];
58
+ if (plain[flag] === undefined && sibling !== undefined) {
59
+ plain[flag] = sibling;
60
+ }
61
+ }
43
62
  return { args: plain, sealed: true };
44
63
  }
45
64
  catch (err) {
@@ -1,6 +1,8 @@
1
1
  import type { PluginRuntime } from '../onboarding/runtime.js';
2
2
  import type { IExchangeAdapter } from '../exchange-adapter.js';
3
3
  import type { TradingMode } from '../types.js';
4
+ import type { HlCredentials } from '../venues/hyperliquid/hl-private.js';
5
+ import { type HlAgentApprovalVerdict } from '../venues/hyperliquid/hl-agent-wallet.js';
4
6
  import { type VenueId } from '../venues/registry.js';
5
7
  export interface SetTradingModeArgs {
6
8
  mode: TradingMode;
@@ -27,6 +29,10 @@ export interface SetTradingModeDeps {
27
29
  * wired to the boot venue and a mixed-venue runtime is not a valid state.
28
30
  * Absent (legacy tests) → guard skipped. */
29
31
  bootVenue?: VenueId;
32
+ /** Injectable approval check (tests). DEFAULTS TO THE REAL ONE — unlike
33
+ * bootVenue this gate is on unless explicitly stubbed, so production
34
+ * wiring can never forget it. */
35
+ hlApprovalCheck?: (creds: HlCredentials) => Promise<HlAgentApprovalVerdict>;
30
36
  /** Override the config file path — tests use this. */
31
37
  configPath?: string;
32
38
  }
@@ -12,6 +12,7 @@
12
12
  // by design: the check lives once, at registration, for all operator tools.
13
13
  import { readPluginConfig } from '../config/plugin-config-io.js';
14
14
  import { validateModeTransition, modeRequiresCredentials } from '../onboarding/mode-ladder.js';
15
+ import { checkHlAgentApproval, } from '../venues/hyperliquid/hl-agent-wallet.js';
15
16
  import { parseVenue } from '../venues/registry.js';
16
17
  import { logger } from '../logger.js';
17
18
  import { recordModeTransition } from '../audit/mode-transition-audit.js';
@@ -133,6 +134,48 @@ export async function setTradingModeTool(args, deps) {
133
134
  reason: 'acknowledgment_required',
134
135
  };
135
136
  }
137
+ // 4.5. HL agent-approval preflight (E2E audit 2026-08-11 #7). An agent key
138
+ // that is not in the master account's extraAgents list — or whose approval
139
+ // expired (they last ≤180 days) — signs orders Hyperliquid rejects one by
140
+ // one: the flip would "succeed", the dashboard would look healthy, and
141
+ // every order would bounce. Refuse ONLY on a definitive negative from the
142
+ // exchange; derivation/network failures fail OPEN with a warning (infra
143
+ // must never block a deliberate operator action — same rule as the
144
+ // credential-entry guards).
145
+ let preflightWarnings = [];
146
+ if (venue === 'hyperliquid' && hlCredentials) {
147
+ let verdict;
148
+ try {
149
+ verdict = await (deps.hlApprovalCheck ?? checkHlAgentApproval)(hlCredentials);
150
+ }
151
+ catch (err) {
152
+ verdict = {
153
+ approved: null,
154
+ validUntil: null,
155
+ warnings: [
156
+ `Approval preflight errored (${err instanceof Error ? err.message : String(err)}) — approval not verified.`,
157
+ ],
158
+ };
159
+ }
160
+ if (verdict.approved === false) {
161
+ recordModeTransition({ previousMode, targetMode: target, acknowledged, ok: false, reason: 'hl_agent_not_approved' });
162
+ const why = verdict.warnings.length > 0 ? ` ${verdict.warnings.join(' ')}` : '';
163
+ return {
164
+ ok: false,
165
+ message: `Hyperliquid reports this agent wallet is NOT approved to trade for ${hlCredentials.walletAddress}.` +
166
+ `${why} Every live order would be rejected. Approve it first: Dashboard → Settings → ` +
167
+ `Exchange connection → Hyperliquid (the guided setup signs one approveAgent transaction), ` +
168
+ `then retry ${target}.`,
169
+ previousMode,
170
+ mode: previousMode,
171
+ readiness: deps.runtime.adapter.readiness,
172
+ reason: 'hl_agent_not_approved',
173
+ };
174
+ }
175
+ preflightWarnings = verdict.warnings;
176
+ for (const w of preflightWarnings)
177
+ logger.warn(TAG, `HL preflight: ${w}`);
178
+ }
136
179
  // 5. Persist the new mode.
137
180
  try {
138
181
  const { updatePluginConfig } = await import('../config/plugin-config-io.js');
@@ -162,7 +205,11 @@ export async function setTradingModeTool(args, deps) {
162
205
  });
163
206
  return {
164
207
  ok: true,
165
- message: `Trading mode changed from ${previousMode} to ${target}`,
208
+ // Preflight warnings (near-expiry approval, zero balance, unverifiable
209
+ // approval) ride the success message — the operator just took a real-money
210
+ // action and these are the things to fix before the first order.
211
+ message: `Trading mode changed from ${previousMode} to ${target}` +
212
+ (preflightWarnings.length > 0 ? `. Note: ${preflightWarnings.join(' ')}` : ''),
166
213
  previousMode,
167
214
  mode: deps.runtime.mode,
168
215
  readiness: deps.runtime.adapter.readiness,
@@ -27,3 +27,29 @@ export declare function __resetKeccakCacheForTests(): void;
27
27
  export declare function deriveAddressFromPrivateKey(privateKey: string): Promise<DeriveAddressResult>;
28
28
  /** Case-insensitive address equality (addresses may arrive checksummed). */
29
29
  export declare function sameAddress(a: string, b: string): boolean;
30
+ /** One-call approval verdict for the STORED credentials — derivation + the
31
+ * unsigned preflight, packaged for the live-flip gate in set_trading_mode
32
+ * (E2E audit 2026-08-11 #7: an unapproved/expired agent key reached LIVE
33
+ * with a healthy-looking dashboard and every order rejected).
34
+ *
35
+ * Lives HERE, not in hl-preflight.ts, because it takes the PRIVATE KEY —
36
+ * hl-preflight's module boundary is addresses-only by design.
37
+ *
38
+ * Verdict semantics (the caller refuses ONLY on `approved === false`):
39
+ * false = the exchange answered definitively — the agent address is not in
40
+ * extraAgents, or its approval has expired. Real money would be
41
+ * un-tradeable; block the flip.
42
+ * null = could not verify (derivation unavailable, HL unreachable,
43
+ * extraAgents unreadable). Fail OPEN — infra must never block a
44
+ * deliberate operator action; the warning travels instead.
45
+ * true = approved (warnings may still carry a near-expiry nudge). */
46
+ export interface HlAgentApprovalVerdict {
47
+ approved: boolean | null;
48
+ validUntil: number | null;
49
+ warnings: string[];
50
+ }
51
+ export declare function checkHlAgentApproval(creds: {
52
+ walletAddress: string;
53
+ agentPrivateKey: string;
54
+ testnet?: boolean;
55
+ }, fetchImpl?: typeof fetch): Promise<HlAgentApprovalVerdict>;
@@ -116,3 +116,35 @@ export async function deriveAddressFromPrivateKey(privateKey) {
116
116
  export function sameAddress(a, b) {
117
117
  return a.trim().toLowerCase() === b.trim().toLowerCase();
118
118
  }
119
+ export async function checkHlAgentApproval(creds, fetchImpl) {
120
+ const derived = await deriveAddressFromPrivateKey(creds.agentPrivateKey);
121
+ if (!derived.ok) {
122
+ return {
123
+ approved: null,
124
+ validUntil: null,
125
+ warnings: [
126
+ derived.reason === 'invalid_key'
127
+ ? 'The stored agent key looks malformed — approval could not be verified.'
128
+ : 'Could not derive the agent wallet address on this host — approval not verified.',
129
+ ],
130
+ };
131
+ }
132
+ const { hlPreflight } = await import('./hl-preflight.js');
133
+ const pre = await hlPreflight({
134
+ walletAddress: creds.walletAddress,
135
+ agentAddress: derived.address,
136
+ testnet: creds.testnet === true,
137
+ fetchImpl,
138
+ });
139
+ if (!pre.reachable) {
140
+ return {
141
+ approved: null,
142
+ validUntil: null,
143
+ warnings: [
144
+ `Could not reach Hyperliquid to verify the agent approval (${pre.unreachableError ?? 'unknown error'}).`,
145
+ ...pre.warnings,
146
+ ],
147
+ };
148
+ }
149
+ return { approved: pre.agentApproved, validUntil: pre.agentValidUntil, warnings: pre.warnings };
150
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
5
5
  "type": "module",
6
6
  "bin": {