openbroker 1.9.2 → 1.9.3

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/SKILL.md +49 -0
  3. package/dist/auto/cli.js +3 -0
  4. package/dist/auto/examples/dca.d.ts +2 -1
  5. package/dist/auto/examples/dca.d.ts.map +1 -1
  6. package/dist/auto/examples/dca.js +19 -1
  7. package/dist/auto/examples/funding-arb.d.ts +2 -1
  8. package/dist/auto/examples/funding-arb.d.ts.map +1 -1
  9. package/dist/auto/examples/funding-arb.js +19 -2
  10. package/dist/auto/examples/grid.d.ts +2 -1
  11. package/dist/auto/examples/grid.d.ts.map +1 -1
  12. package/dist/auto/examples/grid.js +18 -2
  13. package/dist/auto/examples/mm-maker.d.ts +2 -1
  14. package/dist/auto/examples/mm-maker.d.ts.map +1 -1
  15. package/dist/auto/examples/mm-maker.js +18 -2
  16. package/dist/auto/examples/mm-spread.d.ts +2 -1
  17. package/dist/auto/examples/mm-spread.d.ts.map +1 -1
  18. package/dist/auto/examples/mm-spread.js +18 -2
  19. package/dist/auto/examples/price-alert.d.ts +2 -1
  20. package/dist/auto/examples/price-alert.d.ts.map +1 -1
  21. package/dist/auto/examples/price-alert.js +1 -0
  22. package/dist/auto/guardrails.d.ts +19 -0
  23. package/dist/auto/guardrails.d.ts.map +1 -0
  24. package/dist/auto/guardrails.js +575 -0
  25. package/dist/auto/guardrails.test.d.ts +2 -0
  26. package/dist/auto/guardrails.test.d.ts.map +1 -0
  27. package/dist/auto/guardrails.test.js +173 -0
  28. package/dist/auto/loader.d.ts +3 -3
  29. package/dist/auto/loader.d.ts.map +1 -1
  30. package/dist/auto/loader.js +25 -3
  31. package/dist/auto/runtime.d.ts.map +1 -1
  32. package/dist/auto/runtime.js +38 -20
  33. package/dist/auto/types.d.ts +43 -0
  34. package/dist/auto/types.d.ts.map +1 -1
  35. package/dist/lib.d.ts +2 -0
  36. package/dist/lib.d.ts.map +1 -1
  37. package/dist/lib.js +1 -0
  38. package/package.json +4 -3
  39. package/scripts/auto/cli.ts +3 -0
  40. package/scripts/auto/examples/dca.ts +21 -2
  41. package/scripts/auto/examples/funding-arb.ts +21 -3
  42. package/scripts/auto/examples/grid.ts +20 -3
  43. package/scripts/auto/examples/mm-maker.ts +20 -3
  44. package/scripts/auto/examples/mm-spread.ts +20 -3
  45. package/scripts/auto/examples/price-alert.ts +3 -1
  46. package/scripts/auto/guardrails.test.ts +227 -0
  47. package/scripts/auto/guardrails.ts +700 -0
  48. package/scripts/auto/loader.ts +41 -4
  49. package/scripts/auto/runtime.ts +38 -22
  50. package/scripts/auto/types.ts +54 -0
  51. package/scripts/lib.ts +10 -0
@@ -4,7 +4,14 @@ import { existsSync, readdirSync, mkdirSync } from 'fs';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
6
  import { fileURLToPath } from 'url';
7
- import type { AutomationFactory, AutomationConfig } from './types.js';
7
+ import { resolveAutomationGuardrails } from './guardrails.js';
8
+ import type {
9
+ AutomationFactory,
10
+ AutomationConfig,
11
+ AutomationGuardrailContext,
12
+ AutomationGuardrailsExport,
13
+ LoadedAutomation,
14
+ } from './types.js';
8
15
 
9
16
  const __filename = fileURLToPath(import.meta.url);
10
17
  const __dirname = path.dirname(__filename);
@@ -117,8 +124,27 @@ function resolveAutomationConfig(mod: Record<string, unknown>): AutomationConfig
117
124
  return null;
118
125
  }
119
126
 
120
- /** Load an automation module and validate the default export */
121
- export async function loadAutomation(scriptPath: string): Promise<AutomationFactory> {
127
+ function resolveGuardrailsExport(mod: Record<string, unknown>): AutomationGuardrailsExport | null {
128
+ const candidates = [
129
+ mod.guardrails,
130
+ (mod.default as Record<string, unknown> | undefined)?.guardrails,
131
+ (mod["module.exports"] as Record<string, unknown> | undefined)?.guardrails,
132
+ ];
133
+
134
+ for (const candidate of candidates) {
135
+ if (candidate && (typeof candidate === 'object' || typeof candidate === 'function')) {
136
+ return candidate as AutomationGuardrailsExport;
137
+ }
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ /** Load an automation module and validate its factory plus required guardrails export. */
144
+ export async function loadAutomation(
145
+ scriptPath: string,
146
+ context: AutomationGuardrailContext = { config: {} },
147
+ ): Promise<LoadedAutomation> {
122
148
  const absolutePath = path.resolve(scriptPath);
123
149
 
124
150
  // Dynamic import — tsx handles TypeScript transpilation
@@ -132,7 +158,18 @@ export async function loadAutomation(scriptPath: string): Promise<AutomationFact
132
158
  );
133
159
  }
134
160
 
135
- return factory as AutomationFactory;
161
+ const guardrailsExport = resolveGuardrailsExport(mod as Record<string, unknown>);
162
+ if (!guardrailsExport) {
163
+ throw new Error(
164
+ `Automation script must export "guardrails".\n` +
165
+ `Use { mode: "read-only" } for monitoring-only scripts or a validated trading policy.`,
166
+ );
167
+ }
168
+
169
+ return {
170
+ factory: factory as AutomationFactory,
171
+ guardrails: resolveAutomationGuardrails(guardrailsExport, context),
172
+ };
136
173
  }
137
174
 
138
175
  /** List available automation scripts in ~/.openbroker/automations/ */
@@ -13,6 +13,7 @@ import {
13
13
  import { WebSocketManager } from '../core/ws.js';
14
14
  import { AutomationEventBus } from './events.js';
15
15
  import { loadAutomation } from './loader.js';
16
+ import { CLIENT_WRITE_METHODS, createGuardrailedClient } from './guardrails.js';
16
17
  import { registerAutomation, unregisterAutomation, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
17
18
  import { createAutomationAudit, toSerializable, type AutomationAuditSink } from './audit.js';
18
19
  import { startKeepAwake, type KeepAwakeHandle } from './keep-awake.js';
@@ -29,6 +30,7 @@ import type {
29
30
  PublishOptions,
30
31
  ScheduledTask,
31
32
  RunningAutomation,
33
+ LoadedAutomation,
32
34
  } from './types.js';
33
35
 
34
36
  // ── Observer fan-out ────────────────────────────────────────────────
@@ -105,13 +107,6 @@ function fanOutAgentAction(
105
107
  }
106
108
 
107
109
  const STATE_DIR = path.join(os.homedir(), '.openbroker', 'state');
108
- const AUDITED_WRITE_METHODS = new Set([
109
- 'order', 'marketOrder', 'limitOrder', 'triggerOrder',
110
- 'takeProfit', 'stopLoss', 'cancel', 'cancelAll',
111
- 'spotOrder', 'spotMarketOrder', 'spotLimitOrder', 'spotCancel',
112
- 'updateLeverage', 'approveBuilderFee', 'twapOrder', 'twapCancel',
113
- ]);
114
-
115
110
  // ── State persistence ───────────────────────────────────────────────
116
111
 
117
112
  interface StateController {
@@ -202,19 +197,11 @@ function createLogger(id: string, verbose: boolean, audit?: AutomationAuditSink)
202
197
 
203
198
  // ── Dry-run client proxy ────────────────────────────────────────────
204
199
 
205
- const WRITE_METHODS = new Set([
206
- 'order', 'marketOrder', 'limitOrder', 'triggerOrder',
207
- 'takeProfit', 'stopLoss', 'cancel', 'cancelAll',
208
- 'updateLeverage', 'approveBuilderFee',
209
- 'spotOrder', 'spotMarketOrder', 'spotLimitOrder', 'spotCancel',
210
- 'twapOrder', 'twapCancel',
211
- ]);
212
-
213
200
  function createDryClient(client: HyperliquidClient, log: AutomationLogger): HyperliquidClient {
214
201
  return new Proxy(client, {
215
202
  get(target, prop, receiver) {
216
203
  const value = Reflect.get(target, prop, receiver);
217
- if (typeof prop === 'string' && WRITE_METHODS.has(prop) && typeof value === 'function') {
204
+ if (typeof prop === 'string' && CLIENT_WRITE_METHODS.has(prop) && typeof value === 'function') {
218
205
  return (...args: unknown[]) => {
219
206
  log.info(`[DRY] ${prop}(${args.map(a => JSON.stringify(a)).join(', ')})`);
220
207
  return Promise.resolve({ status: 'ok', response: { type: 'dry_run' } });
@@ -234,7 +221,7 @@ function createAuditedClient(
234
221
  return new Proxy(client, {
235
222
  get(target, prop, receiver) {
236
223
  const value = Reflect.get(target, prop, receiver);
237
- if (typeof prop === 'string' && AUDITED_WRITE_METHODS.has(prop) && typeof value === 'function') {
224
+ if (typeof prop === 'string' && CLIENT_WRITE_METHODS.has(prop) && typeof value === 'function') {
238
225
  return async (...args: unknown[]) => {
239
226
  const actionId = `${prop}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
240
227
  audit.recordAction({
@@ -531,8 +518,38 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
531
518
  }
532
519
  }
533
520
  const observers = await loadConventionObservers(log);
521
+ let loaded: LoadedAutomation;
522
+ try {
523
+ log.info(`Loading automation: ${scriptPath}`);
524
+ loaded = await loadAutomation(scriptPath, { config: stateController.snapshot() });
525
+ } catch (err) {
526
+ const error = err instanceof Error ? err : new Error(String(err));
527
+ audit.recordError('guardrail_validation', error);
528
+ await audit.stop({
529
+ status: 'error',
530
+ stopReason: 'guardrail_validation_error',
531
+ pollCount: 0,
532
+ eventsEmitted: 0,
533
+ });
534
+ keepAwake?.stop();
535
+ throw error;
536
+ }
537
+ audit.recordNote('guardrails', loaded.guardrails);
534
538
  const baseClient = dryRun ? createDryClient(rawClient, log) : rawClient;
535
- const client = createAuditedClient(baseClient, audit, dryRun, observers);
539
+ const guardedClient = createGuardrailedClient(baseClient, {
540
+ policy: loaded.guardrails,
541
+ rawClient,
542
+ log,
543
+ onViolation: (error, method, args) => {
544
+ audit.recordNote('guardrail_block', {
545
+ code: error.code,
546
+ message: error.message,
547
+ method,
548
+ args: toSerializable(args),
549
+ });
550
+ },
551
+ });
552
+ const client = createAuditedClient(guardedClient, audit, dryRun, observers);
536
553
 
537
554
  const startHooks: Array<() => void | Promise<void>> = [];
538
555
  const stopHooks: Array<() => void | Promise<void>> = [];
@@ -565,13 +582,12 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
565
582
  audit: auditApi,
566
583
  id,
567
584
  dryRun,
585
+ guardrails: loaded.guardrails,
568
586
  };
569
587
 
570
588
  try {
571
- // Load and execute the factory function (registers handlers)
572
- log.info(`Loading automation: ${scriptPath}`);
573
- const factory = await loadAutomation(scriptPath);
574
- await factory(api);
589
+ // Execute the already validated factory function (registers handlers).
590
+ await loaded.factory(api);
575
591
 
576
592
  // Call onStart hooks
577
593
  for (const hook of startHooks) {
@@ -8,6 +8,57 @@ import type { HyperliquidClient } from '../core/client.js';
8
8
  /** What an automation .ts file exports */
9
9
  export type AutomationFactory = (api: AutomationAPI) => void | Promise<void>;
10
10
 
11
+ /** Runtime-enforced policy exported by every automation module. */
12
+ export type AutomationGuardrails =
13
+ | ReadOnlyAutomationGuardrails
14
+ | TradingAutomationGuardrails;
15
+
16
+ export interface ReadOnlyAutomationGuardrails {
17
+ /** Read-only automations cannot call any client write method. */
18
+ mode: 'read-only';
19
+ }
20
+
21
+ export interface TradingAutomationGuardrails {
22
+ mode: 'trading';
23
+ /** Canonical markets this automation may trade: `ETH`, `xyz:CL`, `spot:HYPE`, `#1230`. */
24
+ allowedMarkets: string[];
25
+ /** Maximum USD notional for one submitted order. */
26
+ maxOrderUsd: number;
27
+ /** Maximum absolute USD exposure in any one market after an order. */
28
+ maxPositionUsd: number;
29
+ /** Maximum absolute USD exposure across the account after an order. */
30
+ maxTotalExposureUsd: number;
31
+ /** Maximum leverage the automation may request or use when increasing perp exposure. */
32
+ maxLeverage: number;
33
+ /** Maximum account margin utilization allowed after a risk-increasing perp order. */
34
+ maxMarginUsedPct: number;
35
+ /** Maximum account-wide open orders after this automation submits an order. */
36
+ maxOpenOrders: number;
37
+ /** Maximum risk-increasing order submissions in a rolling 60-second window. */
38
+ maxOrdersPerMinute: number;
39
+ /** Maximum slippage passed to market-order helpers. */
40
+ maxSlippageBps: number;
41
+ /** Whether market-order helpers may be used. */
42
+ allowMarketOrders: boolean;
43
+ /** Whether `cancelAll()` without a market or an armed `scheduleCancel()` is allowed. */
44
+ allowAccountWideCancel: boolean;
45
+ }
46
+
47
+ export interface AutomationGuardrailContext {
48
+ /** Persisted automation state with current `--set` overrides applied. */
49
+ config: Readonly<Record<string, unknown>>;
50
+ }
51
+
52
+ /** Guardrails may be static or derived from startup config, but the result is always validated. */
53
+ export type AutomationGuardrailsExport =
54
+ | AutomationGuardrails
55
+ | ((context: AutomationGuardrailContext) => AutomationGuardrails);
56
+
57
+ export interface LoadedAutomation {
58
+ factory: AutomationFactory;
59
+ guardrails: AutomationGuardrails;
60
+ }
61
+
11
62
  /** Config field descriptor for example automations */
12
63
  export interface AutomationConfigField {
13
64
  type: 'string' | 'number' | 'boolean';
@@ -206,6 +257,9 @@ export interface AutomationAPI {
206
257
 
207
258
  /** True if running in --dry mode (write methods are intercepted) */
208
259
  dryRun: boolean;
260
+
261
+ /** Validated policy currently enforced by the runtime client proxy. */
262
+ guardrails: Readonly<AutomationGuardrails>;
209
263
  }
210
264
 
211
265
  // ── Runtime internals ───────────────────────────────────────────────
package/scripts/lib.ts CHANGED
@@ -79,6 +79,16 @@ export {
79
79
  loadAutomation,
80
80
  } from './auto/loader.js';
81
81
 
82
+ export {
83
+ GuardrailViolation,
84
+ CLIENT_WRITE_METHODS,
85
+ canonicalMarket,
86
+ validateAutomationGuardrails,
87
+ resolveAutomationGuardrails,
88
+ createGuardrailedClient,
89
+ } from './auto/guardrails.js';
90
+ export type { GuardrailedClientOptions } from './auto/guardrails.js';
91
+
82
92
  export {
83
93
  registerAutomation,
84
94
  unregisterAutomation,