@liquiditytech/rapidx-cli 1.0.34 → 1.0.36

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.
@@ -8,6 +8,11 @@ export const CAPABILITIES = [
8
8
  { capabilityId: "auth.check", cliCommand: "rapidx auth check", mcpTool: "rapidx/self-check", operationType: "DIAGNOSTIC", riskLevel: "read", inputSchema: "AuthInput", outputSchema: "AuthResult", previewRequired: false },
9
9
  { capabilityId: "doctor.run", cliCommand: "rapidx doctor --json", mcpTool: "rapidx/self-check", operationType: "DIAGNOSTIC", riskLevel: "read", inputSchema: "DoctorInput", outputSchema: "DoctorReport", previewRequired: false },
10
10
  { capabilityId: "update.check", cliCommand: "rapidx update check", mcpTool: "rapidx/update/check", operationType: "DIAGNOSTIC", riskLevel: "read", inputSchema: "UpdateCheckInput", outputSchema: "UpdateCheckResult", previewRequired: false },
11
+ { capabilityId: "automation.start", cliCommand: "rapidx automation start", mcpTool: "rapidx/automation/start", operationType: "TRADE_WRITE", riskLevel: "critical-trade-write", inputSchema: "AutomationStartInput", outputSchema: "AutomationSession", previewRequired: false },
12
+ { capabilityId: "automation.list", cliCommand: "rapidx automation list", mcpTool: "rapidx/automation/list", operationType: "READ", riskLevel: "read", inputSchema: "AutomationListInput", outputSchema: "AutomationSessionList", previewRequired: false },
13
+ { capabilityId: "automation.status", cliCommand: "rapidx automation status", mcpTool: "rapidx/automation/status", operationType: "READ", riskLevel: "read", inputSchema: "AutomationStatusInput", outputSchema: "AutomationSession", previewRequired: false },
14
+ { capabilityId: "automation.extend", cliCommand: "rapidx automation extend", mcpTool: "rapidx/automation/extend", operationType: "TRADE_WRITE", riskLevel: "critical-trade-write", inputSchema: "AutomationExtendInput", outputSchema: "AutomationSession", previewRequired: false },
15
+ { capabilityId: "automation.stop", cliCommand: "rapidx automation stop", mcpTool: "rapidx/automation/stop", operationType: "DIAGNOSTIC", riskLevel: "write-config", inputSchema: "AutomationStopInput", outputSchema: "AutomationSession", previewRequired: false },
11
16
  { capabilityId: "schema.discover", cliCommand: "rapidx schema --json", mcpTool: "rapidx/tools", operationType: "READ", riskLevel: "read", inputSchema: "SchemaQuery", outputSchema: "SchemaResult", previewRequired: false },
12
17
  { capabilityId: "self-check.run", cliCommand: "rapidx self-check --read-only --json", mcpTool: "rapidx/self-check", operationType: "READ", riskLevel: "read", inputSchema: "SelfCheckInput", outputSchema: "SelfCheckReport", previewRequired: false },
13
18
  { capabilityId: "market.ticker", cliCommand: "rapidx market get-ticker", mcpTool: "rapidx/market/get-ticker", operationType: "READ", riskLevel: "read", inputSchema: "SymbolInput", outputSchema: "MarketTicker", previewRequired: false },
@@ -1,7 +1,7 @@
1
1
  import { SCHEMA_VERSION } from "./types.js";
2
2
  import { RAPIDX_VERSION } from "../version.js";
3
3
  export const RAPIDX_SKILLS_DISTRIBUTION = "github";
4
- export const RAPIDX_SKILLS_VERSION = "1.0.9";
4
+ export const RAPIDX_SKILLS_VERSION = "1.0.10";
5
5
  export const RAPIDX_SKILLS_SCHEMA_VERSION = "1.0.0";
6
6
  export function buildCompatibilityReport(input = {}) {
7
7
  const checks = [
@@ -68,6 +68,10 @@ const automationScopeSchema = {
68
68
  type: "string",
69
69
  description: "Optional automation scope label, for example single-preview, strategy-session, or the user's exact scope phrase."
70
70
  };
71
+ const automationSessionIdSchema = {
72
+ type: "string",
73
+ description: "Automation session id returned by rapidx automation start. Preview binds to this session and submit re-checks the session before writing."
74
+ };
71
75
  const previewIdSchema = {
72
76
  type: "string",
73
77
  description: "Preview id returned by the matching preview tool or command.",
@@ -118,6 +122,9 @@ const closePositionReduceOnlySchema = {
118
122
  };
119
123
  const algoConditionTypeSchema = { type: "string", enum: ["CONDITIONAL", "OCO", "ENTIRE_CLOSE_POSITION", "PARTIAL_CLOSE_POSITION"] };
120
124
  const triggerTypeSchema = { type: "string", enum: ["LAST_PRICE", "MARK_PRICE"] };
125
+ const symbolsArraySchema = { type: "array", items: symbolSchema, description: "RapidX symbols authorized for this automation session." };
126
+ const stringArraySchema = { type: "array", items: stringSchema };
127
+ const expiresInSecondsSchema = { type: "number", description: "Automation session duration in seconds. Default is 86400; maximum is 2592000." };
121
128
  export function inputSchemaForName(name) {
122
129
  switch (name) {
123
130
  case "EmptyInput":
@@ -312,6 +319,30 @@ export function inputSchemaForName(name) {
312
319
  return objectSchema({ commandLine: stringSchema, preflightError: stringSchema });
313
320
  case "UpdateCheckInput":
314
321
  return objectSchema({ force: booleanSchema, manifestUrl: stringSchema, maxCacheAgeSeconds: numberSchema, installedSkillsVersion: stringSchema });
322
+ case "AutomationStartInput":
323
+ return objectSchema({
324
+ symbols: symbolsArraySchema,
325
+ maxNotionalPerOrder: maxNotionalSchema,
326
+ maxTotalNotional: maxNotionalSchema,
327
+ expiresInSeconds: expiresInSecondsSchema,
328
+ allowedActions: stringArraySchema,
329
+ allowedOrderTypes: stringArraySchema,
330
+ name: stringSchema,
331
+ explicitUserConsent: explicitUserConsentSchema,
332
+ acceptedRiskText: acceptedRiskTextSchema
333
+ }, ["symbols", "maxNotionalPerOrder", "maxTotalNotional", "explicitUserConsent", "acceptedRiskText"]);
334
+ case "AutomationListInput":
335
+ return objectSchema({});
336
+ case "AutomationStatusInput":
337
+ case "AutomationStopInput":
338
+ return objectSchema({ automationSessionId: automationSessionIdSchema }, ["automationSessionId"]);
339
+ case "AutomationExtendInput":
340
+ return objectSchema({
341
+ automationSessionId: automationSessionIdSchema,
342
+ expiresInSeconds: expiresInSecondsSchema,
343
+ explicitUserConsent: explicitUserConsentSchema,
344
+ acceptedRiskText: acceptedRiskTextSchema
345
+ }, ["automationSessionId", "expiresInSeconds", "explicitUserConsent", "acceptedRiskText"]);
315
346
  case "SchemaQuery":
316
347
  return objectSchema({ consumer: stringSchema, consumerVersion: stringSchema, includeExamples: booleanSchema });
317
348
  case "SelfCheckInput":
@@ -432,6 +463,7 @@ function tradeCreateProperties() {
432
463
  }
433
464
  function automationProperties() {
434
465
  return {
466
+ automationSessionId: automationSessionIdSchema,
435
467
  automationMode: automationModeSchema,
436
468
  automationConsentText: automationConsentTextSchema,
437
469
  automationScope: automationScopeSchema
@@ -451,6 +483,9 @@ function matchesType(value, schema) {
451
483
  if (!schema.type) {
452
484
  return true;
453
485
  }
486
+ if (schema.type === "array") {
487
+ return Array.isArray(value);
488
+ }
454
489
  return typeof value === schema.type;
455
490
  }
456
491
  function hasValue(value) {
@@ -16,6 +16,7 @@ export * from "./client/order-id.js";
16
16
  export * from "./client/symbol.js";
17
17
  export * from "./safety/policy.js";
18
18
  export * from "./safety/raw-api.js";
19
+ export * from "./automation/session.js";
19
20
  export * from "./trading/preview.js";
20
21
  export * from "./trading/preview-preflight.js";
21
22
  export * from "./update/check-update.js";
@@ -92,7 +92,7 @@ export function evaluateSafety(capability, input, policy, state = makeSafetyStat
92
92
  }
93
93
  }
94
94
  const clientOrderId = typeof input.clientOrderId === "string" ? input.clientOrderId : undefined;
95
- if (clientOrderId) {
95
+ if (clientOrderId && tracksDuplicateClientOrderId(capability)) {
96
96
  const now = state.nowMs();
97
97
  const seenAt = state.recentClientOrderIds.get(clientOrderId);
98
98
  if (seenAt && now - seenAt < policy.duplicateWindowMs) {
@@ -168,6 +168,11 @@ function requiresNotionalLimit(capability) {
168
168
  || capability.capabilityId === "algo.place"
169
169
  || capability.capabilityId === "position.close";
170
170
  }
171
+ function tracksDuplicateClientOrderId(capability) {
172
+ return capability.capabilityId === "order.place-preview"
173
+ || capability.capabilityId === "order.place"
174
+ || capability.capabilityId === "algo.place";
175
+ }
171
176
  function estimateDirectNotional(input) {
172
177
  const symbol = typeof input.symbol === "string" ? input.symbol : "";
173
178
  if (!symbol.startsWith("BINANCE_")) {
@@ -170,7 +170,11 @@ function validateQuantity(input, rules) {
170
170
  const quantityText = String(quantityValue);
171
171
  const quantity = positiveNumber(quantityText);
172
172
  if (quantity === undefined) {
173
- return;
173
+ throw new ProductError({
174
+ code: "RCORE24006",
175
+ status: "BLOCKED",
176
+ message: `INVALID_QUANTITY: quantity ${quantityText} must be a positive finite number.`
177
+ });
174
178
  }
175
179
  const minSize = positiveNumber(rules.minSize);
176
180
  if (minSize !== undefined && quantity < minSize - 1e-12) {
@@ -204,7 +208,11 @@ function validatePrice(input, rules) {
204
208
  const priceText = String(input.price);
205
209
  const price = positiveNumber(priceText);
206
210
  if (price === undefined) {
207
- return;
211
+ throw new ProductError({
212
+ code: "RCORE24007",
213
+ status: "BLOCKED",
214
+ message: `INVALID_PRICE: price ${priceText} must be a positive finite number.`
215
+ });
208
216
  }
209
217
  const tickSize = positiveNumber(rules.tickSize);
210
218
  if (tickSize !== undefined && !isAlignedToStep(price, tickSize)) {
@@ -5,6 +5,7 @@ import { assertInputMatchesSchema } from "../contracts/input-schema.js";
5
5
  import { evaluateSafety, stableParamsHash } from "../safety/policy.js";
6
6
  import { ProductError } from "../errors/product-error.js";
7
7
  import { parseRapidXSymbol } from "../client/symbol.js";
8
+ import { automationPreviewDetails } from "../automation/session.js";
8
9
  export function makePreviewStore() {
9
10
  return { records: new Map() };
10
11
  }
@@ -37,13 +38,14 @@ export function savePreviewStoreToFile(filePath, store) {
37
38
  const records = [...store.records.values()];
38
39
  writeFileSync(filePath, `${JSON.stringify(records, null, 2)}\n`, { mode: 0o600 });
39
40
  }
40
- export function createTradePreview(capability, input, policy, state, store, ttlSeconds = 300) {
41
+ export function createTradePreview(capability, input, policy, state, store, ttlSeconds = 300, options = {}) {
41
42
  assertInputMatchesSchema(capability.inputSchema, input, {
42
- allowedExtraFields: ["explicitUserConsent", "acceptedRiskText", "automationMode", "automationConsentText", "automationScope"]
43
+ allowedExtraFields: ["explicitUserConsent", "acceptedRiskText", "automationMode", "automationConsentText", "automationScope", "automationSessionId"]
43
44
  });
44
45
  const tradeParams = normalizeTradeParams(input);
45
46
  const automation = makeAutomationDetails(input, tradeParams);
46
- const previewDetails = makePreviewDetails(capability, tradeParams, automation);
47
+ const automationSession = options.automationSession ? automationPreviewDetails(options.automationSession) : undefined;
48
+ const previewDetails = makePreviewDetails(capability, tradeParams, automation, automationSession);
47
49
  const decision = evaluateSafety(capability, input, policy, state);
48
50
  if (!decision.allowed) {
49
51
  throw new ProductError({
@@ -61,13 +63,44 @@ export function createTradePreview(capability, input, policy, state, store, ttlS
61
63
  expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
62
64
  submitted: false,
63
65
  status: "PASS",
64
- confirmation: makeSubmitConfirmation(previewId, automation),
65
- ...(automation ? { automation } : {})
66
+ confirmation: makeSubmitConfirmation(previewId, automation, automationSession),
67
+ ...(automation ? { automation } : {}),
68
+ ...(automationSession ? { automationSession } : {})
66
69
  };
67
70
  store.records.set(previewId, record);
68
71
  return record;
69
72
  }
70
- export function createTargetedTradePreview(targetCapability, input, policy, state, store, ttlSeconds = 300) {
73
+ export function createTargetedTradePreview(targetCapability, input, policy, state, store, ttlSeconds = 300, options = {}) {
74
+ assertTargetedTradePreviewInput(targetCapability, input);
75
+ const tradeParams = normalizeTradeParams(input);
76
+ const automation = makeAutomationDetails(input, tradeParams);
77
+ const automationSession = options.automationSession ? automationPreviewDetails(options.automationSession) : undefined;
78
+ const previewDetails = makePreviewDetails(targetCapability, tradeParams, automation, automationSession);
79
+ const decision = evaluateSafety(targetCapability, input, policy, state);
80
+ if (!decision.allowed) {
81
+ throw new ProductError({
82
+ code: decision.code ?? "RCORE20002",
83
+ status: "BLOCKED",
84
+ message: decision.reason ?? "Preview blocked by safety policy."
85
+ });
86
+ }
87
+ const previewId = `rpv_${randomUUID()}`;
88
+ const record = {
89
+ previewId,
90
+ capabilityId: targetCapability.capabilityId,
91
+ paramsHash: stableParamsHash(tradeParams),
92
+ ...previewDetails,
93
+ expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
94
+ submitted: false,
95
+ status: "PASS",
96
+ confirmation: makeSubmitConfirmation(previewId, automation, automationSession),
97
+ ...(automation ? { automation } : {}),
98
+ ...(automationSession ? { automationSession } : {})
99
+ };
100
+ store.records.set(previewId, record);
101
+ return record;
102
+ }
103
+ export function assertTargetedTradePreviewInput(targetCapability, input) {
71
104
  assertInputMatchesSchema(targetCapability.inputSchema, input, {
72
105
  allowedExtraFields: [
73
106
  "targetCapabilityId",
@@ -79,7 +112,8 @@ export function createTargetedTradePreview(targetCapability, input, policy, stat
79
112
  "skillsSchemaVersion",
80
113
  "automationMode",
81
114
  "automationConsentText",
82
- "automationScope"
115
+ "automationScope",
116
+ "automationSessionId"
83
117
  ],
84
118
  ignoredRequiredFields: ["previewId", "continueConsentId"]
85
119
  });
@@ -97,39 +131,16 @@ export function createTargetedTradePreview(targetCapability, input, policy, stat
97
131
  message: "trade preview target cannot be another preview capability."
98
132
  });
99
133
  }
100
- const tradeParams = normalizeTradeParams(input);
101
- const automation = makeAutomationDetails(input, tradeParams);
102
- const previewDetails = makePreviewDetails(targetCapability, tradeParams, automation);
103
- const decision = evaluateSafety(targetCapability, input, policy, state);
104
- if (!decision.allowed) {
105
- throw new ProductError({
106
- code: decision.code ?? "RCORE20002",
107
- status: "BLOCKED",
108
- message: decision.reason ?? "Preview blocked by safety policy."
109
- });
110
- }
111
- const previewId = `rpv_${randomUUID()}`;
112
- const record = {
113
- previewId,
114
- capabilityId: targetCapability.capabilityId,
115
- paramsHash: stableParamsHash(tradeParams),
116
- ...previewDetails,
117
- expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
118
- submitted: false,
119
- status: "PASS",
120
- confirmation: makeSubmitConfirmation(previewId, automation),
121
- ...(automation ? { automation } : {})
122
- };
123
- store.records.set(previewId, record);
124
- return record;
125
134
  }
126
- function makeSubmitConfirmation(previewId, automation) {
135
+ function makeSubmitConfirmation(previewId, automation, automationSession) {
127
136
  return {
128
137
  submitToken: `confirm_${previewId}`,
129
138
  requiredFields: ["previewId", "continueConsentId"],
130
- instruction: automation
131
- ? "Automation mode is enabled for this preview. The agent may pass submitToken as continueConsentId without asking for another per-order chat confirmation, within the authorized scope."
132
- : "Pass submitToken as continueConsentId only after the user has confirmed this exact preview."
139
+ instruction: automationSession
140
+ ? "This preview is authorized by an active automation session. The agent may pass submitToken as continueConsentId without asking for another per-order chat confirmation, within the session scope."
141
+ : automation
142
+ ? "Automation mode is enabled for this preview. The agent may pass submitToken as continueConsentId without asking for another per-order chat confirmation, within the authorized scope."
143
+ : "Pass submitToken as continueConsentId only after the user has confirmed this exact preview."
133
144
  };
134
145
  }
135
146
  export function verifyPreview(store, previewId, capability, input, now = new Date()) {
@@ -175,9 +186,23 @@ export function verifyPreview(store, previewId, capability, input, now = new Dat
175
186
  }
176
187
  export function consumePreview(store, previewId, capability, input, now = new Date()) {
177
188
  const record = verifyPreview(store, previewId, capability, input, now);
189
+ assertSubmitToken(record, input);
178
190
  store.records.delete(record.previewId);
179
191
  return record;
180
192
  }
193
+ function assertSubmitToken(record, input) {
194
+ const expected = record.confirmation?.submitToken;
195
+ if (!expected) {
196
+ return;
197
+ }
198
+ if (input.continueConsentId !== expected) {
199
+ throw new ProductError({
200
+ code: "RCORE20002",
201
+ status: "BLOCKED",
202
+ message: "continueConsentId must match the preview confirmation.submitToken."
203
+ });
204
+ }
205
+ }
181
206
  function normalizeTradeParams(input) {
182
207
  const normalized = {};
183
208
  for (const [key, value] of Object.entries(input)) {
@@ -195,11 +220,11 @@ function canonicalSymbolOrOriginal(value) {
195
220
  return value;
196
221
  }
197
222
  }
198
- function makePreviewDetails(capability, tradeParams, automation) {
223
+ function makePreviewDetails(capability, tradeParams, automation, automationSession) {
199
224
  return {
200
225
  businessParams: { ...tradeParams },
201
226
  requestSummary: makeRequestSummary(capability.capabilityId, tradeParams),
202
- riskNotes: makeRiskNotes(capability.capabilityId, tradeParams, automation)
227
+ riskNotes: makeRiskNotes(capability.capabilityId, tradeParams, automation, automationSession)
203
228
  };
204
229
  }
205
230
  function makeRequestSummary(capabilityId, params) {
@@ -259,8 +284,11 @@ function makeRequestSummary(capabilityId, params) {
259
284
  }
260
285
  return compactRecord({ action: capabilityId, ...params });
261
286
  }
262
- function makeRiskNotes(capabilityId, params, automation) {
287
+ function makeRiskNotes(capabilityId, params, automation, automationSession) {
263
288
  const notes = [];
289
+ if (automationSession) {
290
+ notes.push("Automation session allows the agent to submit this preview with submitToken without asking for another per-order chat confirmation.");
291
+ }
264
292
  if (automation) {
265
293
  notes.push("Automation mode allows the agent to submit this preview with submitToken without asking for another per-order chat confirmation.");
266
294
  }
@@ -388,7 +416,8 @@ const NON_TRADE_HASH_FIELDS = new Set([
388
416
  "skillsSchemaVersion",
389
417
  "automationMode",
390
418
  "automationConsentText",
391
- "automationScope"
419
+ "automationScope",
420
+ "automationSessionId"
392
421
  ]);
393
422
  function isTradePreviewRecord(value) {
394
423
  if (!value || typeof value !== "object") {
@@ -1 +1 @@
1
- export const RAPIDX_VERSION = "1.0.34";
1
+ export const RAPIDX_VERSION = "1.0.36";
@@ -29,6 +29,11 @@ const TOOL_DESCRIPTIONS = {
29
29
  "schema.discover": `Discover the RapidX MCP tool surface, capability metadata, and concrete JSON input schemas. Use this before constructing tool inputs. Schema version: ${SCHEMA_VERSION}.`,
30
30
  "self-check.run": "Run a read-only RapidX integration self-check. Verifies tool discovery, schema, credential configuration, and optional live read probes without submitting trade writes.",
31
31
  "update.check": "Check the RapidX release manifest for CLI, MCP schema, and skills versions. Use during setup, review, or session startup; do not run before every trade submit.",
32
+ "automation.start": "Create a bounded local RapidX automation session for agent trading. The user must authorize symbols, per-order notional, total notional, duration, allowed actions, and allowed order types before this tool is called. Use order.place/order.replace/order.cancel for normal order lifecycle automation.",
33
+ "automation.list": "List local RapidX automation sessions in the current workspace, including active, stopped, and expired sessions.",
34
+ "automation.status": "Read one RapidX automation session by automationSessionId, including remaining scope, usedNotional, and expiration.",
35
+ "automation.extend": "Extend an active RapidX automation session after the user authorizes a new duration. This cannot expand symbols, actions, order types, or notional limits.",
36
+ "automation.stop": "Stop a RapidX automation session. Stopping blocks future automation previews and submits for that session; it does not cancel existing orders.",
32
37
  "market.ticker": "Read the latest ticker for a RapidX symbol. Public market adapter may query Binance or OKX venue APIs and returns canonical RapidX symbol plus original venue symbol when they differ.",
33
38
  "market.orderbook": "Read the order book for a RapidX symbol. Public market adapter may query Binance or OKX venue depth/books endpoints and returns canonical RapidX symbol plus venue data.",
34
39
  "market.klines": "Read historical candlesticks for a RapidX symbol. Requires symbol and interval, with optional limit. Public market adapter currently supports Binance-style kline data.",
@@ -43,9 +48,9 @@ const TOOL_DESCRIPTIONS = {
43
48
  "portfolio.position-bracket": "Read RapidX position tier/bracket information for a symbol from GET /api/v1/trading/positionBracket.",
44
49
  "portfolio.set-position-mode": "Preview-required write: change RapidX account position mode through POST /api/v1/trading/account. Use only when the user explicitly asks to change account position mode.",
45
50
  "trade.preview": "Create a generic preview token by targetCapabilityId for write operations without a dedicated order preview command, including portfolio writes, position writes, and algo writes before submitting the target tool.",
46
- "order.place-preview": "Preview an order placement before submitting a real order. Validates input schema, local safety rules, symbol trading rules from GET /api/v1/trading/sym/info, maxNotional, and returns previewId plus confirmation.submitToken.",
47
- "order.replace-preview": "Preview an order replacement before submitting a real replace. Performs order readback through GET /api/v1/trading/order to verify the order exists and is replaceable, then returns previewId plus confirmation.submitToken.",
48
- "order.cancel-preview": "Preview an order cancellation before submitting a real cancel. Performs order readback through GET /api/v1/trading/order to verify the order exists and is cancelable, then returns previewId plus confirmation.submitToken.",
51
+ "order.place-preview": "Preview an order placement before submitting a real order. Validates input schema, local safety rules, optional automationSessionId scope, symbol trading rules from GET /api/v1/trading/sym/info, maxNotional, and returns previewId plus confirmation.submitToken.",
52
+ "order.replace-preview": "Preview an order replacement before submitting a real replace. Performs order readback through GET /api/v1/trading/order to verify the order exists and is replaceable, checks optional automationSessionId scope, then returns previewId plus confirmation.submitToken. Automated replace consumes replacement order notional.",
53
+ "order.cancel-preview": "Preview an order cancellation before submitting a real cancel. Performs order readback through GET /api/v1/trading/order to verify the order exists and is cancelable, checks optional automationSessionId scope, then returns previewId plus confirmation.submitToken. Automated cancel consumes no notional.",
49
54
  "order.place": "Preview-required write: submit a real order to POST /api/v1/trading/order. Must use unchanged business parameters from order.place-preview and continueConsentId=confirmation.submitToken.",
50
55
  "order.replace": "Preview-required write: replace a real order through PUT /api/v1/trading/order. Must use unchanged business parameters from order.replace-preview or a matching trade preview.",
51
56
  "order.cancel": "Preview-required write: cancel a real order through DELETE /api/v1/trading/order. Cancel can be asynchronous; inspect cancelAccepted, terminalStateConfirmed, and poll order.query when needed.",
@@ -1,4 +1,4 @@
1
- import { consumePreview, createTargetedTradePreview, createTradePreview, defaultSafetyPolicy, executeRapidXCapability, findCapabilityById, getSchemaResult, makeEvidence, makePreviewStore, makeSafetyState, normalizeUnknownError, publicErrorDetails, runSelfCheck, runPreviewPreflight, runTradingVerification, verifyPreview, buildLiveReadOnlySelfCheck, buildLiveTradingVerificationProbes, checkForUpdate, } from "../core/index.js";
1
+ import { consumePreview, createTargetedTradePreview, createTradePreview, defaultSafetyPolicy, executeRapidXCapability, findCapabilityById, assertAutomationSessionStillAllowsSubmit, assertTargetedTradePreviewInput, extendAutomationSession, getAutomationSession, getSchemaResult, listAutomationSessions, loadAutomationSessionStoreFromFile, makeEvidence, makePreviewStore, makeSafetyState, normalizeUnknownError, ProductError, publicErrorDetails, recordAutomationSessionSubmit, resolveAutomationSessionForPreview, resolveAutomationSessionStoreFile, runSelfCheck, runPreviewPreflight, runTradingVerification, verifyPreview, saveAutomationSessionStoreToFile, startAutomationSession, stopAutomationSession, withAutomationSessionStoreLock, buildLiveReadOnlySelfCheck, buildLiveTradingVerificationProbes, checkForUpdate, } from "../core/index.js";
2
2
  import { capabilityForTool, getMcpToolDefinitions } from "./tool-registry.js";
3
3
  import { writeMcpAudit } from "./audit.js";
4
4
  const previewStore = makePreviewStore();
@@ -48,6 +48,9 @@ export async function runMcpTool(toolName, input = {}) {
48
48
  const auditId = writeMcpAudit("update-check", status, { toolName, updateStatus: update.status });
49
49
  return { ok: status === "PASS", status, data: update, auditId, evidence: [makeEvidence(toolName)] };
50
50
  }
51
+ if (toolName.startsWith("rapidx/automation/")) {
52
+ return runAutomationTool(toolName, input);
53
+ }
51
54
  if (toolName === "rapidx/trading-verification" || toolName === "rapidx/trade/verify-live") {
52
55
  const report = await runTradingVerification(input, buildLiveTradingVerificationProbes(input));
53
56
  const auditId = writeMcpAudit("trading-verification", report.status, { toolName, status: report.status });
@@ -59,7 +62,9 @@ export async function runMcpTool(toolName, input = {}) {
59
62
  if (!previewCapability) {
60
63
  throw new Error("order.place-preview capability missing");
61
64
  }
62
- const preview = createTradePreview(previewCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
65
+ const automationStore = loadAutomationSessionStoreFromFile(resolveAutomationSessionStoreFile());
66
+ const automation = optionalAutomationSession(automationStore, "order.place", input);
67
+ const preview = createTradePreview(previewCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore, 300, automation ? { automationSession: automation.session } : {});
63
68
  try {
64
69
  Object.assign(preview, await runPreviewPreflight("order.place", input));
65
70
  }
@@ -76,9 +81,13 @@ export async function runMcpTool(toolName, input = {}) {
76
81
  if (!targetCapability) {
77
82
  throw new Error(`${concretePreviewTarget} capability missing`);
78
83
  }
79
- const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
84
+ assertTargetedTradePreviewInput(targetCapability, input);
85
+ const preflight = await runPreviewPreflight(concretePreviewTarget, input);
86
+ const automationStore = loadAutomationSessionStoreFromFile(resolveAutomationSessionStoreFile());
87
+ const automation = optionalAutomationSession(automationStore, concretePreviewTarget, { ...input, ...preflight });
88
+ const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore, 300, automation ? { automationSession: automation.session } : {});
80
89
  try {
81
- Object.assign(preview, await runPreviewPreflight(concretePreviewTarget, input));
90
+ Object.assign(preview, preflight);
82
91
  }
83
92
  catch (error) {
84
93
  previewStore.records.delete(preview.previewId);
@@ -96,9 +105,13 @@ export async function runMcpTool(toolName, input = {}) {
96
105
  if (targetCapability.operationType !== "TRADE_WRITE" || isPreviewCapability(targetCapability.capabilityId)) {
97
106
  throw new Error("trade preview target must be a non-preview trade write capability.");
98
107
  }
99
- const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore);
108
+ assertTargetedTradePreviewInput(targetCapability, input);
109
+ const preflight = await runPreviewPreflight(targetCapability.capabilityId, input);
110
+ const automationStore = loadAutomationSessionStoreFromFile(resolveAutomationSessionStoreFile());
111
+ const automation = optionalAutomationSession(automationStore, targetCapability.capabilityId, { ...input, ...preflight });
112
+ const preview = createTargetedTradePreview(targetCapability, input, { ...defaultSafetyPolicy(), tradingEnabled: true, readOnly: false }, safetyState, previewStore, 300, automation ? { automationSession: automation.session } : {});
100
113
  try {
101
- Object.assign(preview, await runPreviewPreflight(targetCapability.capabilityId, input));
114
+ Object.assign(preview, preflight);
102
115
  }
103
116
  catch (error) {
104
117
  previewStore.records.delete(preview.previewId);
@@ -107,15 +120,45 @@ export async function runMcpTool(toolName, input = {}) {
107
120
  const auditId = writeMcpAudit("trade-preview", "PASS", { toolName, capabilityId: targetCapability.capabilityId, previewId: preview.previewId });
108
121
  return { ok: true, status: "PASS", data: preview, auditId, evidence: [makeEvidence(toolName)] };
109
122
  }
123
+ let consumedPreview;
110
124
  if (capability.previewRequired) {
111
- verifyPreview(previewStore, typeof input.previewId === "string" ? input.previewId : undefined, capability, input);
125
+ const automationStore = loadAutomationSessionStoreFromFile(resolveAutomationSessionStoreFile());
126
+ const previewRecord = verifyPreview(previewStore, typeof input.previewId === "string" ? input.previewId : undefined, capability, input);
127
+ assertAutomationSessionStillAllowsSubmit(automationStore, previewRecord, capability.capabilityId);
112
128
  const consentId = input.continueConsentId;
113
129
  if (typeof consentId !== "string" || consentId.length === 0) {
114
130
  return { ok: false, status: "BLOCKED", code: "RMCP20001", message: "continueConsentId is required for trade writes.", evidence: [makeEvidence(toolName)] };
115
131
  }
116
- consumePreview(previewStore, typeof input.previewId === "string" ? input.previewId : undefined, capability, input);
132
+ if (previewRecord.automationSession) {
133
+ const automationStoreFile = resolveAutomationSessionStoreFile();
134
+ const data = await withAutomationSessionStoreLock(automationStoreFile, async () => {
135
+ const lockedAutomationStore = loadAutomationSessionStoreFromFile(automationStoreFile);
136
+ const lockedPreviewRecord = verifyPreview(previewStore, typeof input.previewId === "string" ? input.previewId : undefined, capability, input);
137
+ assertAutomationSessionStillAllowsSubmit(lockedAutomationStore, lockedPreviewRecord, capability.capabilityId);
138
+ const lockedConsumedPreview = consumePreview(previewStore, typeof input.previewId === "string" ? input.previewId : undefined, capability, input);
139
+ const result = await executeRapidXCapability(capability.capabilityId, input);
140
+ recordAutomationSessionSubmit(lockedAutomationStore, lockedConsumedPreview, new Date(), capability.capabilityId);
141
+ saveAutomationSessionStoreToFile(automationStoreFile, lockedAutomationStore);
142
+ return result;
143
+ });
144
+ const auditId = writeMcpAudit("tool-call", "PASS", { toolName, capabilityId: capability.capabilityId });
145
+ return {
146
+ ok: true,
147
+ status: "PASS",
148
+ data,
149
+ auditId,
150
+ evidence: [makeEvidence(toolName, "real_tool_call")]
151
+ };
152
+ }
153
+ consumedPreview = consumePreview(previewStore, typeof input.previewId === "string" ? input.previewId : undefined, capability, input);
117
154
  }
118
155
  const data = await executeRapidXCapability(capability.capabilityId, input);
156
+ if (consumedPreview?.automationSession) {
157
+ const automationStoreFile = resolveAutomationSessionStoreFile();
158
+ const automationStore = loadAutomationSessionStoreFromFile(automationStoreFile);
159
+ recordAutomationSessionSubmit(automationStore, consumedPreview, new Date(), capability.capabilityId);
160
+ saveAutomationSessionStoreToFile(automationStoreFile, automationStore);
161
+ }
119
162
  const auditId = writeMcpAudit("tool-call", "PASS", { toolName, capabilityId: capability.capabilityId });
120
163
  return {
121
164
  ok: true,
@@ -147,6 +190,46 @@ function concretePreviewTargetForTool(toolName) {
147
190
  }
148
191
  return undefined;
149
192
  }
193
+ function runAutomationTool(toolName, input) {
194
+ const action = toolName.split("/").at(-1) ?? "";
195
+ const filePath = resolveAutomationSessionStoreFile();
196
+ return withAutomationSessionStoreLock(filePath, () => {
197
+ const store = loadAutomationSessionStoreFromFile(filePath);
198
+ if (action === "start") {
199
+ const session = startAutomationSession(store, input);
200
+ saveAutomationSessionStoreToFile(filePath, store);
201
+ return { ok: true, status: "PASS", data: session, evidence: [makeEvidence(toolName)] };
202
+ }
203
+ if (action === "list") {
204
+ return { ok: true, status: "PASS", data: { sessions: listAutomationSessions(store) }, evidence: [makeEvidence(toolName)] };
205
+ }
206
+ if (action === "status") {
207
+ return { ok: true, status: "PASS", data: getAutomationSession(store, input), evidence: [makeEvidence(toolName)] };
208
+ }
209
+ if (action === "extend") {
210
+ const session = extendAutomationSession(store, input);
211
+ saveAutomationSessionStoreToFile(filePath, store);
212
+ return { ok: true, status: "PASS", data: session, evidence: [makeEvidence(toolName)] };
213
+ }
214
+ if (action === "stop") {
215
+ const session = stopAutomationSession(store, input);
216
+ saveAutomationSessionStoreToFile(filePath, store);
217
+ return { ok: true, status: "PASS", data: session, evidence: [makeEvidence(toolName)] };
218
+ }
219
+ throw new Error(`Unknown automation tool: ${toolName}`);
220
+ });
221
+ }
222
+ function optionalAutomationSession(store, targetCapabilityId, input) {
223
+ try {
224
+ return resolveAutomationSessionForPreview(store, targetCapabilityId, input);
225
+ }
226
+ catch (error) {
227
+ if (error instanceof ProductError && error.code === "RCORE26001") {
228
+ return undefined;
229
+ }
230
+ throw error;
231
+ }
232
+ }
150
233
  function tradingVerificationEnvelope(toolName, report, auditId) {
151
234
  if (report.status === "PASS") {
152
235
  return { ok: true, status: "PASS", data: report, auditId, evidence: [makeEvidence(toolName)] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liquiditytech/rapidx-cli",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "RapidX CLI, MCP server mode, registry, and release assets.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -48,13 +48,13 @@ rapidx portfolio set-position-mode --input @/absolute/path/set-position-mode.jso
48
48
  When checking an existing installation, read the installed RapidX skill `version` from the loaded `SKILL.md` frontmatter and pass it to update check:
49
49
 
50
50
  ```bash
51
- rapidx update check --input '{"installedSkillsVersion":"1.0.9"}' --json
51
+ rapidx update check --input '{"installedSkillsVersion":"1.0.10"}' --json
52
52
  ```
53
53
 
54
54
  For upgrade reviews, force a remote manifest refresh:
55
55
 
56
56
  ```bash
57
- rapidx update check --input '{"installedSkillsVersion":"1.0.9","force":true}' --json
57
+ rapidx update check --input '{"installedSkillsVersion":"1.0.10","force":true}' --json
58
58
  ```
59
59
 
60
60
  Update output fields:
@@ -84,7 +84,6 @@ When using `orderId`, pass the RapidX 16-digit numeric order id. Invalid `orderI
84
84
 
85
85
  `rapidx transaction executions` reads transaction executions from `GET /api/v1/trading/executions`.
86
86
 
87
- Automation mode still uses preview. Pass `automationMode=true` and the user's exact `automationConsentText` to preview only when the user has enabled automation in chat for the current scope.
88
- Valid automation preview returns an `automation` object with `confirmationMode="automation-preview"`; generic or missing automation consent text is blocked before submission.
87
+ Automation session still uses preview. Create a bounded session with `rapidx automation start` only after the user authorizes the scope; the input must include `explicitUserConsent=true` and `acceptedRiskText`. Pass `automationSessionId` to order place/replace/cancel preview, then submit the returned `previewId` and `confirmation.submitToken` without another per-order chat confirmation. Use `rapidx automation extend` only after the user authorizes more time and include a new `acceptedRiskText`; use `rapidx automation stop` when the user stops automation. For order lifecycle automation, include `allowedActions:["order.place","order.replace","order.cancel"]`; place consumes notional by `maxNotional`, replace consumes the replacement order notional, and cancel consumes no notional.
89
88
 
90
89
  `rapidx position close` is a close-position action. Do not pass `side` or `quantity`; RapidX determines BUY or SELL from the current position and closes the target symbol/positionSide. Use a reduce-only order flow for partial closes. Verify the final exposure with `rapidx position query --json`, and do not rely only on `order query` to interpret the close intent.
@@ -31,8 +31,8 @@ Key trading tools:
31
31
  - `rapidx/trade/preview` creates generic preview tokens by `targetCapabilityId` for write operations without a dedicated order preview command, including position, portfolio, and algo writes.
32
32
  - Preview ids are local to the running MCP server. Do not submit a CLI-created preview id through MCP, and do not submit an MCP-created preview id through CLI.
33
33
  - `rapidx/order/cancel` returns cancel acceptance plus terminal-state guidance. Poll `rapidx/order/query` when `terminalStateConfirmed=false`.
34
- - Automation mode still uses preview. Set `automationMode=true` and pass the user's exact `automationConsentText` only when the user has enabled automation in chat for the current scope.
35
- - Valid automation preview returns an `automation` object with `confirmationMode="automation-preview"`; generic or missing automation consent text is blocked before submission.
34
+ - Automation session still uses preview. Create a bounded session with `rapidx/automation/start` only after the user authorizes the scope; the input must include `explicitUserConsent=true` and `acceptedRiskText`. Pass `automationSessionId` to order place/replace/cancel preview, then submit the returned `previewId` and `confirmation.submitToken` without another per-order chat confirmation. Include `allowedActions:["order.place","order.replace","order.cancel"]` when the automation should manage an order lifecycle.
35
+ - Use `rapidx/automation/status`, `rapidx/automation/extend`, and `rapidx/automation/stop` for session lifecycle management. `rapidx/automation/extend` also requires `explicitUserConsent=true` and a new `acceptedRiskText`.
36
36
  - `rapidx/position/history` reads historical positions.
37
37
  - `rapidx/order/history` and `rapidx/algo/history` accept optional `begin` and `end` time range fields in milliseconds. If omitted, RapidX applies the upstream server default range.
38
38
  - `rapidx/transaction/executions` reads transaction executions from `GET /api/v1/trading/executions`.
@@ -130,7 +130,13 @@ Preview ids are runtime-local. Submit MCP previews through the same MCP server r
130
130
 
131
131
  `order.cancel` is asynchronous. If the cancel result says `terminalStateConfirmed=false`, poll `order query` until a terminal state before claiming that the order is cancelled.
132
132
 
133
- For automation, keep the preview-first flow. Use `automationMode=true` and the user's exact `automationConsentText` only after the user has enabled RapidX automation mode in chat for the current scope.
133
+ For automation, keep the preview-first flow. Create a bounded automation session first, then pass `automationSessionId` into order place/replace/cancel preview input. If the preview returns `automationSession.confirmationMode="automation-session"` and `confirmation.submitToken`, submit that preview without another per-order chat confirmation.
134
+
135
+ ```bash
136
+ rapidx automation start --input '{"symbols":["BINANCE_PERP_BTC_USDT"],"maxNotionalPerOrder":"100","maxTotalNotional":"1000","expiresInSeconds":3600,"allowedActions":["order.place","order.replace","order.cancel"],"allowedOrderTypes":["MARKET","LIMIT"],"explicitUserConsent":true,"acceptedRiskText":"I authorize RapidX automation for BINANCE_PERP_BTC_USDT with maxNotionalPerOrder 100 and maxTotalNotional 1000."}' --json
137
+ rapidx order place-preview --input '{"automationSessionId":"<automationSessionId>","symbol":"BINANCE_PERP_BTC_USDT","side":"BUY","orderType":"MARKET","amount":"50","maxNotional":"60","clientOrderId":"auto-001"}' --json
138
+ rapidx order place --input '{"symbol":"BINANCE_PERP_BTC_USDT","side":"BUY","orderType":"MARKET","amount":"50","maxNotional":"60","clientOrderId":"auto-001","previewId":"<previewId>","continueConsentId":"<confirmation.submitToken>"}' --json
139
+ ```
134
140
 
135
141
  `maxNotional` is a safety upper bound, not the target order amount. If a requested amount is below the symbol `minNotional`, ask the user to confirm the larger amount before submitting. For `position.close`, do not pass `side` or `quantity`; RapidX determines the close side from the current position and closes the target symbol/positionSide. Use a reduce-only order flow for partial closes, then check final exposure with `position query`.
136
142